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

- 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:
2026-09-26 22:22:40 +08:00
parent abbdb39acc
commit 336664473b
9 changed files with 236 additions and 12 deletions
+41 -1
View File
@@ -33,6 +33,7 @@ class RestApiIntegrationTest extends TestCase {
'zone' => 'hot',
'column' => 'rp_price',
'type' => 'decimal',
'show_in_rest' => true,
] );
$registry->register( 'integration_rest', [
'post_type' => 'restapi',
@@ -40,11 +41,21 @@ class RestApiIntegrationTest extends TestCase {
'zone' => 'hot',
'column' => 'rp_featured',
'type' => 'tinyint',
'show_in_rest' => true,
] );
$registry->register( 'integration_rest', [
'post_type' => 'restapi',
'meta_key' => 'rp_description',
'zone' => 'cold',
'show_in_rest' => true,
] );
// Registered but not public (1.1.1: fields are private by default).
$registry->register( 'integration_rest', [
'post_type' => 'restapi',
'meta_key' => 'rp_cost',
'zone' => 'hot',
'column' => 'rp_cost',
'type' => 'decimal',
] );
// Create hot table.
@@ -53,6 +64,7 @@ class RestApiIntegrationTest extends TestCase {
post_id BIGINT UNSIGNED NOT NULL,
rp_price DECIMAL(10,2) DEFAULT NULL,
rp_featured TINYINT(1) DEFAULT NULL,
rp_cost DECIMAL(10,2) DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
@@ -91,7 +103,8 @@ class RestApiIntegrationTest extends TestCase {
$featured = $i % 2;
$status = $statuses[ $i ] ?? 'publish';
$wpdb->query( "INSERT INTO `{$wpdb->posts}` (ID, post_type, post_status) VALUES ($i, 'restapi', '$status')" );
$wpdb->query( "INSERT INTO `" . self::$hot_table . "` (post_id, rp_price, rp_featured) VALUES ($i, $price, $featured)" );
$cost = 1000 - $price;
$wpdb->query( "INSERT INTO `" . self::$hot_table . "` (post_id, rp_price, rp_featured, rp_cost) VALUES ($i, $price, $featured, $cost)" );
}
$wpdb->query( "INSERT INTO `{$wpdb->posts}` (ID, post_type, post_status) VALUES (9, 'post', 'publish')" );
$wpdb->query( "INSERT INTO `" . self::$hot_table . "` (post_id, rp_price, rp_featured) VALUES (9, 900, 1)" );
@@ -194,6 +207,33 @@ class RestApiIntegrationTest extends TestCase {
$this->assertTrue( true );
}
public function test_listings_never_return_a_private_column(): void {
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
$req->set_param( 'post_type', 'restapi' );
foreach ( self::$api->get_listings( $req )->get_data() as $item ) {
$this->assertArrayNotHasKey( 'rp_cost', $item );
}
}
public function test_private_column_cannot_be_used_as_a_filter(): void {
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
$req->set_param( 'post_type', 'restapi' );
$req->set_param( 'rp_cost_min', 800 );
$this->assertSame( '5', self::$api->get_listings( $req )->get_headers()['X-WP-Total'], 'filter on a private column is ignored' );
}
public function test_private_column_cannot_be_used_for_ordering(): void {
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
$req->set_param( 'post_type', 'restapi' );
$req->set_param( 'orderby', 'rp_cost' );
$req->set_param( 'order', 'ASC' );
// rp_cost descends as post_id rises; ignoring it falls back to post_id ASC.
$this->assertSame( array( 1, 2, 3, 4, 5 ), array_column( self::$api->get_listings( $req )->get_data(), 'id' ) );
}
public function test_listings_filter_price_min(): void {
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
$req->set_param( 'post_type', 'restapi' );