post_type )->cap->create_posts ); } /** * @param WP_REST_Request $request * * @return bool */ public function update_item_permissions_check( WP_REST_Request $request ): bool { return current_user_can( 'edit_post', (int) $request['id'] ); } /** * @param WP_REST_Request $request * * @return bool */ public function delete_item_permissions_check( WP_REST_Request $request ): bool { return current_user_can( 'delete_post', (int) $request['id'] ); } /** * Retrieves a collection of ads. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function get_items( WP_REST_Request $request ): WP_REST_Response|WP_Error { $query_args = $this->build_query_args( $request ); $query = new WP_Query( $query_args ); if ( ! $query->have_posts() ) { $response = new WP_REST_Response( [], 200 ); $response->header( 'X-WP-Total', 0 ); $response->header( 'X-WP-TotalPages', 0 ); return $response; } $ad_ids = wp_list_pluck( $query->posts, 'ID' ); _prime_post_caches( $ad_ids, false, true ); $used_in_placements = adpresso_get_placement_repository()->get_used_in_map( $ad_ids, 'ad', [ 'any' ] ); $used_in_groups = adpresso_get_group_repository()->find_containing_ads_map( $ad_ids, [ 'publish', 'draft' ] ); $ad_repository = adpresso_get_ad_repository(); $items = []; foreach ( $query->posts as $post ) { // TODO: if we set process_data flag to false, conditions won't be summarized. $ad = $ad_repository->get( $post->ID, true ); if ( $ad ) { $ad->set_used_in_override( $used_in_placements[ $post->ID ] ?? [], $used_in_groups[ $post->ID ] ?? [] ); // condition_builder_data/edit_ui_sections are only needed by the edit modal (get_item()), not the list. $items[] = $ad->prepare_for_rest_response( false ); } } $response = new WP_REST_Response( $items, 200 ); $response->header( 'X-WP-Total', $query->found_posts ); $response->header( 'X-WP-TotalPages', $query->max_num_pages ); return $response; } /** * Retrieves a single ad. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function get_item( WP_REST_Request $request ): WP_REST_Response|WP_Error { $ad = adpresso_get_ad( (int) $request['id'] ); if ( ! $ad ) { return new WP_Error( 'rest_not_found', __( 'Ad not found.', 'adpresso' ), [ 'status' => 404 ] ); } return new WP_REST_Response( $ad->prepare_for_rest_response(), 200 ); } /** * Creates a single ad. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function create_item( WP_REST_Request $request ): WP_REST_Response|WP_Error { // Create a new, empty Ad entity to pass to the repository $ad = new Ad( null ); $saved_ad = adpresso_get_ad_repository()->save( $ad, $request->get_params() ); if ( ! $saved_ad ) { return new WP_Error( 'rest_cannot_create', __( 'Failed to create ad.', 'adpresso' ), [ 'status' => 500 ] ); } $skipped = $this->apply_assignment_changes( $request->get_param( 'assignment_changes' ), $saved_ad->get_id() ); $response = new WP_REST_Response( $this->with_skipped_assignments( $saved_ad->prepare_for_rest_response(), $skipped ), 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $saved_ad->get_id() ) ) ); return $response; } /** * Updates a single ad. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function update_item( WP_REST_Request $request ): WP_REST_Response|WP_Error { $ad = adpresso_get_ad( (int) $request['id'] ); if ( ! $ad ) { return new WP_Error( 'rest_not_found', __( 'Ad not found.', 'adpresso' ), [ 'status' => 404 ] ); } $ad_id = $ad->get_id(); $skipped = $this->apply_assignment_changes( $request->get_param( 'assignment_changes' ), $ad_id ); $saved_ad = adpresso_get_ad_repository()->save( $ad, $request->get_params() ); if ( ! $saved_ad ) { return new WP_Error( 'rest_cannot_update', __( 'Failed to update ad.', 'adpresso' ), [ 'status' => 500 ] ); } return new WP_REST_Response( $this->with_skipped_assignments( $saved_ad->prepare_for_rest_response(), $skipped ), 200 ); } /** * Adds the assignment changes that were not applied to the response data. * * @param mixed $data Response data. * @param array $skipped Changes that were not applied. * * @return mixed */ private function with_skipped_assignments( mixed $data, array $skipped ): mixed { if ( $skipped && is_array( $data ) ) { $data['skipped_assignments'] = $skipped; } return $data; } /** * Whether the current user may edit the given post, and it is of one of the given types. * * @param int $post_id Target post. * @param string[] $post_types Allowed post types. * * @return bool */ private function can_edit_target( int $post_id, array $post_types ): bool { return $post_id > 0 && in_array( get_post_type( $post_id ), $post_types, true ) && current_user_can( 'edit_post', $post_id ); } /** * Applies group/placement assignment changes sent by the editor modal. * * @param mixed $assignment_changes Raw value of the 'assignment_changes' request param. * @param int $ad_id * * @return array The changes skipped because the target is not editable for the user (or of the wrong type). */ private function apply_assignment_changes( mixed $assignment_changes, int $ad_id ): array { if ( empty( $assignment_changes ) || ! is_array( $assignment_changes ) ) { return []; } /** @var GroupRepository $group_repository */ $group_repository = adpresso_get_group_repository(); /** @var PlacementRepository $placement_repository */ $placement_repository = adpresso_get_placement_repository(); $skipped = []; foreach ( $assignment_changes as $change ) { if ( ! is_array( $change ) ) { continue; } $action = sanitize_key( $change['action'] ?? '' ); switch ( $action ) { case 'add_to_group': case 'remove_from_group': $group_id = absint( $change['groupId'] ?? 0 ); if ( $group_id <= 0 ) { break; } if ( ! $this->can_edit_target( $group_id, [ Constants::POST_TYPE_GROUP ] ) ) { $skipped[] = [ 'action' => $action, 'groupId' => $group_id, ]; break; } if ( 'add_to_group' === $action ) { $group_repository->add_ad_to_group( $group_id, $ad_id ); } else { $group_repository->remove_ad_from_group( $group_id, $ad_id ); } break; case 'assign_to_placement': $placement_id = absint( $change['placementId'] ?? 0 ); $context_id = absint( $change['context']['id'] ?? 0 ); // The ad's own context has no ID yet when a new ad is saved for the first time. if ( $context_id <= 0 && 'group' !== ( $change['context']['type'] ?? '' ) ) { $context_id = $ad_id; } if ( $placement_id <= 0 || $context_id <= 0 ) { break; } // Both the placement and the assigned item (this ad, or a group) must be editable. if ( ! $this->can_edit_target( $placement_id, [ Constants::POST_TYPE_PLACEMENT ] ) || ! $this->can_edit_target( $context_id, [ Constants::POST_TYPE_AD, Constants::POST_TYPE_GROUP ] ) ) { $skipped[] = [ 'action' => $action, 'placementId' => $placement_id, ]; break; } $placement_repository->assign_item_to_placement( $placement_id, $context_id ); break; case 'unassign_from_placement': $placement_id = absint( $change['placementId'] ?? 0 ); if ( $placement_id <= 0 ) { break; } if ( ! $this->can_edit_target( $placement_id, [ Constants::POST_TYPE_PLACEMENT ] ) ) { $skipped[] = [ 'action' => $action, 'placementId' => $placement_id, ]; break; } $placement_repository->unassign_item_from_placement( $placement_id ); break; } } return $skipped; } /** * Deletes a single ad. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function delete_item( WP_REST_Request $request ): WP_REST_Response|WP_Error { $ad = adpresso_get_ad( (int) $request['id'] ); if ( ! $ad ) { return new WP_Error( 'rest_not_found', __( 'Ad not found.', 'adpresso' ), [ 'status' => 404 ] ); } $previous_data = $ad->prepare_for_rest_response(); $force = (bool) $request['force']; // Same answer as WP core's posts endpoint: trashing twice is a client error, not a server failure. if ( ! $force && get_post_status( $ad->get_id() ) === 'trash' ) { return new WP_Error( 'rest_already_trashed', __( 'The ad has already been moved to the trash.', 'adpresso' ), [ 'status' => 410 ] ); } $result = adpresso_get_ad_repository()->delete( $ad, $force ); if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'Failed to delete ad.', 'adpresso' ), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'deleted' => true, 'previous' => $previous_data ], 200 ); } /** * @return array */ protected function get_collection_params(): array { return [ 'page' => [ 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint' ], 'per_page' => [ 'type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint' ], 'orderby' => [ 'type' => 'string', 'default' => 'date', 'sanitize_callback' => 'sanitize_key' ], 'order' => [ 'type' => 'string', 'default' => 'desc', 'enum' => [ 'asc', 'desc' ] ], 's' => [ 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ], 'filters' => [ 'type' => 'object' ], ]; } /** * @param string $method * * @return array */ protected function get_endpoint_args_for_item_schema( string $method ): array { return [ 'title' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ], 'content' => [ 'type' => 'string' ], // Sanitization happens in repository 'status' => [ 'type' => 'string', 'enum' => [ 'publish', 'draft', 'pending', 'private' ] ], 'meta_data' => [ 'type' => 'object' ], ]; } }