rest_base = 'entries'; } /** * Registers the routes. */ public function register_routes(): void { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => $this->id_arg( __( 'Unique identifier of the entry.', 'devplugins-custom-form-builder' ) ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => $this->id_arg( __( 'Unique identifier of the entry.', 'devplugins-custom-form-builder' ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Checks whether the caller may list entries. * * @param WP_REST_Request $request Current request. * * @return true|WP_Error True when allowed. */ public function get_items_permissions_check( $request ): bool|WP_Error { return $this->authorize( Capabilities::MANAGE_ENTRIES ); } /** * Checks whether the caller may read an entry. * * @param WP_REST_Request $request Current request. * * @return true|WP_Error True when allowed. */ public function get_item_permissions_check( $request ): bool|WP_Error { return $this->authorize( Capabilities::MANAGE_ENTRIES ); } /** * Checks whether the caller may delete an entry. * * @param WP_REST_Request $request Current request. * * @return true|WP_Error True when allowed. */ public function delete_item_permissions_check( $request ): bool|WP_Error { return $this->authorize( Capabilities::MANAGE_ENTRIES ); } /** * Returns a page of entries. * * @param WP_REST_Request $request Current request. */ public function get_items( $request ): WP_REST_Response { $args = array( 'search' => (string) $request->get_param( 'search' ), 'form' => (int) $request->get_param( 'form' ), 'status' => (string) $request->get_param( 'status' ), 'orderby' => (string) $request->get_param( 'orderby' ), 'order' => (string) $request->get_param( 'order' ), 'per_page' => $this->per_page( $request ), 'page' => (int) $request->get_param( 'page' ), ); $entries = $this->submissions->query( $args ); $ids = array(); foreach ( $entries as $entry ) { $ids[] = $entry->id(); } // One meta query for the whole page rather than one per entry. $meta = $this->submissions->meta_for_many( $ids ); $items = array(); foreach ( $entries as $entry ) { $items[] = $this->prepare_entry( $entry, $meta[ $entry->id() ] ?? array() ); } return $this->collection( $items, $this->submissions->count( $args ), (int) $args['per_page'] ); } /** * Returns a single entry. * * @param WP_REST_Request $request Current request. * * @return WP_REST_Response|WP_Error The response. */ public function get_item( $request ): WP_REST_Response|WP_Error { $entry = $this->submissions->find( (int) $request->get_param( 'id' ) ); if ( ! $entry instanceof Submission ) { return $this->not_found( __( 'No entry was found with that ID.', 'devplugins-custom-form-builder' ) ); } return rest_ensure_response( $this->prepare_entry( $entry, $this->submissions->meta( $entry->id() ) ) ); } /** * Deletes an entry, its values and its uploads. * * @param WP_REST_Request $request Current request. * * @return WP_REST_Response|WP_Error The response. */ public function delete_item( $request ): WP_REST_Response|WP_Error { $entry_id = (int) $request->get_param( 'id' ); $entry = $this->submissions->find( $entry_id ); if ( ! $entry instanceof Submission ) { return $this->not_found( __( 'No entry was found with that ID.', 'devplugins-custom-form-builder' ) ); } $previous = $this->prepare_entry( $entry, $this->submissions->meta( $entry_id ) ); if ( ! $this->submissions->delete( $entry_id ) ) { return new WP_Error( 'formpilot_rest_delete_failed', __( 'The entry could not be deleted.', 'devplugins-custom-form-builder' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'deleted' => true, 'previous' => $previous, ) ); } /** * Returns the arguments a collection request accepts. * * @return array> Argument definitions. */ public function get_collection_params(): array { return array_merge( $this->pagination_args(), array( 'form' => array( 'description' => __( 'Limit results to entries of one form.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), 'status' => array( 'description' => __( 'Limit results to entries with a given status.', 'devplugins-custom-form-builder' ), 'type' => 'string', 'default' => '', 'enum' => array_merge( array( '' ), array_keys( Submission::statuses() ) ), ), 'orderby' => array( 'description' => __( 'Column to sort the collection by.', 'devplugins-custom-form-builder' ), 'type' => 'string', 'default' => 'created_at', 'enum' => array( 'id', 'form_id', 'status', 'created_at' ), ), ) ); } /** * Returns the schema of an entry. * * @return array The schema. */ public function get_item_schema(): array { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $this->schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'formpilot-entry', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier of the entry.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'readonly' => true, ), 'form_id' => array( 'description' => __( 'Form the entry belongs to.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'readonly' => true, ), 'user_id' => array( 'description' => __( 'User who submitted, 0 for guests.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'readonly' => true, ), 'status' => array( 'description' => __( 'Whether the entry has been opened.', 'devplugins-custom-form-builder' ), 'type' => 'string', 'readonly' => true, 'enum' => array_keys( Submission::statuses() ), ), 'values' => array( 'description' => __( 'Submitted values, keyed by field key.', 'devplugins-custom-form-builder' ), 'type' => 'object', 'readonly' => true, ), 'ip_address' => array( 'description' => __( 'Stored client address.', 'devplugins-custom-form-builder' ), 'type' => 'string', 'readonly' => true, ), 'post_id' => array( 'description' => __( 'Post the form was submitted from.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'readonly' => true, ), 'created_at' => array( 'description' => __( 'Date the entry was received.', 'devplugins-custom-form-builder' ), 'type' => 'string', 'readonly' => true, ), ), ); return $this->add_additional_fields_schema( $this->schema ); } /** * Turns an entry into its response shape. * * @param Submission $entry Entry to prepare. * @param array $meta Raw stored values. * * @return array The prepared item. */ private function prepare_entry( Submission $entry, array $meta ): array { $values = array(); foreach ( $meta as $key => $raw ) { $value = $this->formatter->decode( $raw ); /* * Only a file this plugin stored is reported as one, with its URL rebuilt * from the stored path, as the entry screen does. A visitor can type * `{"url":"https://evil.example/x","name":"Invoice.pdf","file":"a"}` into * a text field; that decoded into an upload-shaped array and went out with * the attacker's URL for any API client to present as a download. Such a * value is the text that was typed. Where the server keeps a real file is * not part of the answer; the webhook payload drops it too. */ if ( $this->formatter->is_file_payload( $value ) ) { $value = $this->formatter->is_file( $value ) ? array( 'name' => (string) $value['name'], 'url' => $this->formatter->file_url( $value ), 'type' => (string) ( $value['type'] ?? '' ), 'size' => (int) ( $value['size'] ?? 0 ), ) : $raw; } $values[ $key ] = $value; } $data = array( 'id' => $entry->id(), 'form_id' => $entry->form_id(), 'user_id' => $entry->user_id(), 'status' => $entry->status(), 'values' => $values, 'ip_address' => $entry->ip_address(), 'referer' => $entry->referer(), 'post_id' => $entry->post_id(), 'created_at' => $entry->created_at(), 'updated_at' => $entry->updated_at(), ); /** * Filters an entry prepared for the REST API. * * @since 1.0.0 * * @param array $data Prepared item. * @param Submission $entry Entry that was prepared. */ return (array) apply_filters( 'formpilot_rest_prepare_entry', $data, $entry ); } }