Skip to content

REST API: Add source parameter for types endpoint #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6453b6c
Initial commit, still with errors
cbravobernal Apr 29, 2025
dda11f7
Default should be false
cbravobernal Apr 30, 2025
ff4c6b0
Remove unit to try e2e
cbravobernal Apr 30, 2025
5e704c1
Add query parameter to filter by post type registration ori
priethor Apr 30, 2025
9f5576e
Fix PHPCS error
priethor Apr 30, 2025
4c27770
Don't require the sidebar experiment to be enabled
priethor May 5, 2025
f61d429
First attempt at filtering post types by registration origin
priethor May 5, 2025
bad53df
Refactor to make it more elegant
priethor May 5, 2025
1e128a3
Optimization
priethor May 5, 2025
9b29cdb
Add origin to schema
priethor May 5, 2025
a4fcb14
Clean up response
priethor May 5, 2025
4386792
Simplify and use a more elegant filter
priethor May 5, 2025
a3b901a
Fix single type queries
priethor May 5, 2025
bc62377
Cleanup
priethor May 5, 2025
1b30485
Initial commit, still with errors
cbravobernal Apr 29, 2025
4938c71
Default should be false
cbravobernal Apr 30, 2025
08c005c
Remove unit to try e2e
cbravobernal Apr 30, 2025
53f3531
Clean up response
priethor May 5, 2025
1ccf496
Simplify and use a more elegant filter
priethor May 5, 2025
cebf109
Fix single type queries
priethor May 5, 2025
978e331
Cleanup
priethor May 5, 2025
2b76395
Resolved conflicts from rebase
priethor May 5, 2025
7382fcf
Fix the scf fields experiment
priethor May 5, 2025
b75c136
Rename `origin` to `source`
priethor May 6, 2025
2a2dfe1
Add unit and e2e tests
priethor May 8, 2025
70133ce
Optimization!
priethor May 8, 2025
2263520
Fix error in comments
priethor May 12, 2025
f5b72e5
Remove checks for SCF internal types, we only work with managed CPTs
priethor May 12, 2025
cbcc576
Update version references to specify SCF
priethor May 12, 2025
c0b5198
Undo changes done by mistake
priethor May 12, 2025
3dc5034
Comment cleanup
priethor May 19, 2025
602ffa4
Merge trunk into add/origin-parameter-for-types-endpoint
priethor May 19, 2025
bbd12f5
Fix `@since`annotation
priethor May 21, 2025
49c2460
Remove console logging from the tests
priethor May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ assets/build/**/*.php
/blob-report/
/playwright/.cache/
/artifacts/

# Claude settings
**/.claude/settings.local.json
2 changes: 1 addition & 1 deletion includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* REST API
*
* @package Secure Custom Fields
* @since ACF 6.4.0
* @since SCF 6.5.0
*/

// Exit if accessed directly.
Expand Down
270 changes: 269 additions & 1 deletion includes/rest-api/class-acf-rest-types-endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,174 @@
/**
* Class SCF_Rest_Types_Endpoint
*
* Extends the /wp/v2/types endpoint to include SCF fields.
* Extends the /wp/v2/types endpoint to include SCF fields and source filtering.
*
* @since SCF 6.5.0
*/
class SCF_Rest_Types_Endpoint {

/**
* Cached post types for the current request
*
* @var array|null
*/
private $cached_post_types = null;

/**
* Initialize the class.
*
* @since SCF 6.5.0
*/
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_extra_fields' ) );
add_action( 'rest_api_init', array( $this, 'register_parameters' ) );

// Add filter to process REST API requests by route
add_filter( 'rest_request_before_callbacks', array( $this, 'filter_types_request' ), 10, 3 );

// Add filter to process each post type individually
add_filter( 'rest_prepare_post_type', array( $this, 'filter_post_type' ), 10, 3 );

// Clean up null entries from the response
add_filter( 'rest_pre_echo_response', array( $this, 'clean_types_response' ), 10, 3 );
}

/**
* Filter post types requests (both collection and individual)
*
* @since SCF 6.5.0
*
* @param mixed $response The current response, either response or null.
* @param array $handler The handler for the route.
* @param WP_REST_Request $request The request object.
* @return mixed The response or null.
*/
public function filter_types_request( $response, $handler, $request ) {
// Check if this is a types endpoint request
$route = $request->get_route();
$is_collection = '/wp/v2/types' === $route;
$is_single_type = preg_match( '#^/wp/v2/types/([^/]+)$#', $route, $matches );

if ( ! $is_collection && ! $is_single_type ) {
return $response;
}

// Get the source parameter
$source = $request->get_param( 'source' );

// Only proceed if source parameter is provided and valid
if ( ! $source || ! in_array( $source, array( 'core', 'scf', 'other' ), true ) ) {
return $response;
}

// Get post types, calculating once and reusing for the entire request
if ( null === $this->cached_post_types ) {
$this->cached_post_types = $this->get_source_post_types( $source );
}
$source_post_types = $this->cached_post_types;

// For single post type requests, check if it matches the source
if ( $is_single_type && isset( $matches[1] ) ) {
$requested_type = $matches[1];

// If the requested type doesn't match the source, return 404
if ( ! in_array( $requested_type, $source_post_types, true ) ) {
return new WP_Error(
'rest_post_type_invalid',
__( 'Invalid post type.', 'secure-custom-fields' ),
array( 'status' => 404 )
);
}
}
// For collection requests, we don't need to add any filter here
// as clean_types_response will handle removing null values from the response
// and filter_post_type will handle individual filtering

return $response;
}

/**
* Filter individual post type in the response.
*
* @since SCF 6.5.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post_Type $post_type The post type object.
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response|null The filtered response or null to filter it out.
*/
public function filter_post_type( $response, $post_type, $request ) {
$source = $request->get_param( 'source' );

// Only apply filtering if source parameter is provided and valid
if ( ! $source || ! in_array( $source, array( 'core', 'scf', 'other' ), true ) ) {
return $response;
}

if ( null === $this->cached_post_types ) {
$this->cached_post_types = $this->get_source_post_types( $source );
}
$source_post_types = $this->cached_post_types;

if ( ! in_array( $post_type->name, $source_post_types, true ) ) {
return null;
}

return $response;
}

/**
* Get an array of post types for each source.
*
* @since SCF 6.5.0
*
* @param string $source The source to get post types for.
* @return array An array of post type names for the specified source.
*/
private function get_source_post_types( $source ) {

$core_types = array();
$scf_types = array();

if ( 'core' === $source || 'other' === $source ) {
$all_post_types = get_post_types( array( '_builtin' => true ), 'objects' );
foreach ( $all_post_types as $post_type ) {
$core_types[] = $post_type->name;
}
}

if ( 'scf' === $source || 'other' === $source ) {
$scf_types = array();

// Get SCF-managed post types
if ( function_exists( 'acf_get_internal_post_type_posts' ) ) {
$scf_managed_post_types = acf_get_internal_post_type_posts( 'acf-post-type' );
foreach ( $scf_managed_post_types as $scf_post_type ) {
if ( isset( $scf_post_type['post_type'] ) ) {
$scf_types[] = $scf_post_type['post_type'];
}
}
}
}

switch ( $source ) {
case 'core':
$result = $core_types;
break;
case 'scf':
$result = $scf_types;
break;
case 'other':
$result = array_diff(
array_keys( get_post_types( array(), 'objects' ) ),
array_merge( $core_types, $scf_types )
);
break;
default:
$result = array();
}

return $result;
}

/**
Expand All @@ -40,6 +195,7 @@ public function register_extra_fields() {
if ( ! (bool) get_option( 'scf_beta_feature_editor_sidebar_enabled', false ) ) {
return;
}

register_rest_field(
'type',
'scf_field_groups',
Expand Down Expand Up @@ -123,4 +279,116 @@ private function get_field_schema() {
'context' => array( 'view', 'edit', 'embed' ),
);
}

/**
* Register the source parameter for the post types endpoint.
*
* @since SCF 6.5.0
*/
public function register_parameters() {
if ( ! acf_get_setting( 'rest_api_enabled' ) ) {
return;
}

// Register the query parameter with the REST API
add_filter( 'rest_type_collection_params', array( $this, 'add_collection_params' ) );
add_filter( 'rest_types_collection_params', array( $this, 'add_collection_params' ) );

// Direct registration for OpenAPI documentation
add_filter( 'rest_endpoints', array( $this, 'add_parameter_to_endpoints' ) );
}

/**
* Get the source parameter definition
*
* @since SCF 6.5.0
*
* @param bool $include_validation Whether to include validation callbacks.
* @return array Parameter definition
*/
private function get_source_param_definition( $include_validation = false ) {
$param = array(
'description' => __( 'Filter post types by their source.', 'secure-custom-fields' ),
'type' => 'string',
'enum' => array( 'core', 'scf', 'other' ),
'required' => false,
);

// Not needed for OpenAPI documentation
if ( $include_validation ) {
$param['validate_callback'] = 'rest_validate_request_arg';
$param['sanitize_callback'] = 'sanitize_text_field';
$param['default'] = null;
$param['in'] = 'query';
}

return $param;
}

/**
* Add source parameter directly to the endpoints for proper documentation
*
* @since SCF 6.5.0
*
* @param array $endpoints The REST API endpoints.
* @return array Modified endpoints
*/
public function add_parameter_to_endpoints( $endpoints ) {
$source_param = $this->get_source_param_definition();
$endpoints_to_modify = array( '/wp/v2/types', '/wp/v2/types/(?P<type>[\w-]+)' );

foreach ( $endpoints_to_modify as $route ) {
if ( isset( $endpoints[ $route ] ) ) {
foreach ( $endpoints[ $route ] as &$endpoint ) {
if ( isset( $endpoint['args'] ) ) {
$endpoint['args']['source'] = $source_param;
}
}
}
}

return $endpoints;
}

/**
* Add source parameter to the collection parameters for the types endpoint.
*
* @since SCF 6.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @return array Modified collection parameters.
*/
public function add_collection_params( $query_params ) {
$query_params['source'] = $this->get_source_param_definition( true );
return $query_params;
}

/**
* Clean up null entries from the response
*
* @since SCF 6.5.0
*
* @param array $response The response data.
* @param WP_REST_Server $server The REST server instance.
* @param WP_REST_Request $request The original request.
* @return array The filtered response data.
*/
public function clean_types_response( $response, $server, $request ) {
if ( strpos( $request->get_route(), '/wp/v2/types' ) !== 0 ) {
return $response;
}

// Only process collection responses (not single post type responses)
// Single post type responses have a 'slug' property, collections don't
if ( is_array( $response ) && ! isset( $response['slug'] ) ) {
$response = array_filter(
$response,
function ( $entry ) {
return null !== $entry;
}
);
}

return $response;
}
}
Loading
Loading