fix(2meet-data-optimizer): keep registered fields out of the public listings REST endpoints
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 15s
Tests / PHP Lint (pull_request) Successful in 17s
Tests / Unit Tests (pull_request) Successful in 51s
Tests / Integration Tests (pull_request) Successful in 1m22s
Tests / PHPCS (pull_request) Successful in 1m12s
Tests / PHPStan (pull_request) Successful in 1m10s
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 15s
Tests / PHP Lint (pull_request) Successful in 17s
Tests / Unit Tests (pull_request) Successful in 51s
Tests / Integration Tests (pull_request) Successful in 1m22s
Tests / PHPCS (pull_request) Successful in 1m12s
Tests / PHPStan (pull_request) Successful in 1m10s
- show_in_rest defaulted to true, so GET /wpdo/v1/listings and /listings/{id} (public by design) returned every registered hot / cold field to anonymous visitors — e.g. infocards tmeetic_ig_access_token_enc / _token_expiry / _user_id and WooCommerce _purchase_note (confirmed on dev36)
- Fields are private unless registered with show_in_rest => true; protected (underscore) and credential-like keys (token, secret, password, api key, private key, credential, salt, nonce) are never exposed
- Zone A list path no longer returns whole rows when nothing is public, and filters / orderby accept public columns only
- Version 1.1.1; CHANGELOG, readme
- Verify: unit 690 (+3), integration 525 (+3 private-column tests, RED on 1.1.0), live suite rest 2/2 (RED on 1.1.0), all live 49/49, phpcs 0, stan OK
This commit is contained in:
@@ -1396,8 +1396,13 @@ class TMDO_REST_API {
|
||||
): WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
// Filters and ordering only on public columns: hidden values must not be
|
||||
// inferable by filtering or sorting on them.
|
||||
$table = TMDO_Zone_Hot::table( $post_type );
|
||||
$columns = TMDO_Schema_Registry::instance()->get_hot_columns( $post_type );
|
||||
$columns = array_intersect_key(
|
||||
TMDO_Schema_Registry::instance()->get_hot_columns( $post_type ),
|
||||
array_flip( TMDO_Schema_Registry::instance()->get_rest_visible_hot_columns( $post_type ) )
|
||||
);
|
||||
|
||||
// Build WHERE clauses from numeric filter params.
|
||||
$where = array();
|
||||
@@ -1443,8 +1448,8 @@ class TMDO_REST_API {
|
||||
'post_type' => $post_type,
|
||||
);
|
||||
unset( $row['post_id'], $row['updated_at'] );
|
||||
// Keep only columns with show_in_rest => true (or all if no registry entry).
|
||||
$row = $visible_cols ? array_intersect_key( $row, $visible_cols ) : $row;
|
||||
// Keep only public columns (none when nothing is declared public).
|
||||
$row = array_intersect_key( $row, $visible_cols );
|
||||
return array_merge( $item, $row );
|
||||
},
|
||||
$rows
|
||||
|
||||
@@ -103,7 +103,9 @@ class TMDO_Schema_Registry {
|
||||
* - ttl (int) TTL in seconds (Zone B only, null = no expiry).
|
||||
* - cache_group (string) Object cache group (Zone C only).
|
||||
* - cache_ttl (int) Cache TTL in seconds (Zone C only).
|
||||
* - show_in_rest (bool) Whether to expose via GET /wpdo/v1/listings. Default true.
|
||||
* - show_in_rest (bool) Whether to expose via the public GET /wpdo/v1/listings
|
||||
* endpoints. Default false; protected (underscore) keys and
|
||||
* credential-like keys are never exposed (see rest_exposable()).
|
||||
*/
|
||||
public function register( string $provider, array $config ): void {
|
||||
$config = wp_parse_args(
|
||||
@@ -120,7 +122,7 @@ class TMDO_Schema_Registry {
|
||||
'cache_group' => '',
|
||||
'cache_ttl' => HOUR_IN_SECONDS,
|
||||
'provider' => $provider,
|
||||
'show_in_rest' => true,
|
||||
'show_in_rest' => false,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -276,6 +278,22 @@ class TMDO_Schema_Registry {
|
||||
return array_unique( $this->cold_fields[ $post_type ] ?? array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a field may appear in the public listings endpoints: declared
|
||||
* show_in_rest, not a protected meta key (leading underscore — WordPress
|
||||
* hides those from public output, e.g. _purchase_note), and not named like a
|
||||
* credential (tokens, secrets, passwords, API keys), whatever it declares.
|
||||
*
|
||||
* @param array<string, mixed> $field Registered field.
|
||||
* @return bool
|
||||
*/
|
||||
private static function rest_exposable( array $field ): bool {
|
||||
$key = (string) ( $field['meta_key'] ?? '' );
|
||||
return ! empty( $field['show_in_rest'] )
|
||||
&& 0 !== strpos( $key, '_' )
|
||||
&& 1 !== preg_match( '/token|secret|passw|api_?key|private_?key|credential|salt|nonce/i', $key . ' ' . ( $field['column'] ?? '' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hot column names for a post type where show_in_rest is true.
|
||||
* Used by REST endpoints to restrict publicly exposed fields.
|
||||
@@ -286,7 +304,7 @@ class TMDO_Schema_Registry {
|
||||
public function get_rest_visible_hot_columns( string $post_type ): array {
|
||||
$visible = array();
|
||||
foreach ( $this->fields as $field ) {
|
||||
if ( 'hot' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
|
||||
if ( 'hot' === $field['zone'] && $field['post_type'] === $post_type && self::rest_exposable( $field ) ) {
|
||||
$visible[] = $field['column'];
|
||||
}
|
||||
}
|
||||
@@ -303,7 +321,7 @@ class TMDO_Schema_Registry {
|
||||
public function get_rest_visible_cold_keys( string $post_type ): array {
|
||||
$visible = array();
|
||||
foreach ( $this->fields as $field ) {
|
||||
if ( 'cold' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
|
||||
if ( 'cold' === $field['zone'] && $field['post_type'] === $post_type && self::rest_exposable( $field ) ) {
|
||||
$visible[] = $field['meta_key'];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user