From 5c4606fc779e6078efd307c083ea12255472230c Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 3 Sep 2026 19:06:59 -0400 Subject: [PATCH] emcp-tooling --- .../abilities/class-content-abilities.php | 18 +++- includes/class-schema-compat.php | 20 +++++ tests/SanitizeSchemaUnionItemsTypeTest.php | 82 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/SanitizeSchemaUnionItemsTypeTest.php diff --git a/includes/abilities/class-content-abilities.php b/includes/abilities/class-content-abilities.php index c730dd5..efaa8f5 100644 --- a/includes/abilities/class-content-abilities.php +++ b/includes/abilities/class-content-abilities.php @@ -1053,7 +1053,7 @@ class EMCP_Tools_Content_Abilities { 'properties' => array( 'post_id' => array( 'type' => 'integer', 'description' => __( 'The post ID.', 'emcp-tools' ) ), 'taxonomy' => array( 'type' => 'string', 'description' => __( 'Taxonomy name (e.g. category, post_tag).', 'emcp-tools' ) ), - 'terms' => array( 'type' => 'array', 'items' => array( 'type' => array( 'integer', 'string' ) ), 'description' => __( 'Term IDs or names.', 'emcp-tools' ) ), + 'terms' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), 'description' => __( 'Term IDs (as strings) or names. Numeric strings are coerced to IDs in the executor.', 'emcp-tools' ) ), 'mode' => array( 'type' => 'string', 'enum' => array( 'replace', 'append', 'remove' ), 'description' => __( 'Default: replace.', 'emcp-tools' ) ), 'create_missing' => array( 'type' => 'boolean', 'description' => __( 'Create term names that do not exist. Default: true.', 'emcp-tools' ) ), ), @@ -1090,7 +1090,23 @@ class EMCP_Tools_Content_Abilities { $mode = in_array( $mode, array( 'replace', 'append', 'remove' ), true ) ? $mode : 'replace'; $terms = array_values( (array) ( $input['terms'] ?? array() ) ); + // Normalise incoming items: accept IDs and names; if a numeric string is + // supplied, cast to int so the no-create-missing path treats it as an ID + // (matches the existing `is_numeric` branch below for create_missing=false). + // Without this, the create-missing path would try to create a term whose + // name is `"461"`, which wp_insert_term rejects as a bad name. $create_missing = ! isset( $input['create_missing'] ) || (bool) $input['create_missing']; + if ( 'remove' !== $mode && $create_missing ) { + $coerced = array(); + foreach ( $terms as $term ) { + if ( is_numeric( $term ) ) { + $coerced[] = (int) $term; + } else { + $coerced[] = (string) $term; + } + } + $terms = $coerced; + } // When not auto-creating, resolve names to existing term IDs and drop // any name that doesn't exist (numeric IDs pass through untouched). diff --git a/includes/class-schema-compat.php b/includes/class-schema-compat.php index 475c77a..93969f9 100644 --- a/includes/class-schema-compat.php +++ b/includes/class-schema-compat.php @@ -189,6 +189,26 @@ class EMCP_Tools_Schema_Compat { } } + // Flatten `type: [a, b, …]` (union) to a single non-null type. The WordPress + // Abilities API input validator on WP 7.1 rejects array-typed `items.type` + // with a generic "input[X] is not of type array" error before the executor + // runs. Picking the first non-null entry is the documented "lossy" way to + // express the union without losing the schema altogether. Callers that need + // both types should coerce at the executor (see `set-post-terms`). + if ( isset( $schema['type'] ) && is_array( $schema['type'] ) ) { + $flattened = null; + foreach ( $schema['type'] as $candidate ) { + if ( is_string( $candidate ) && 'null' !== $candidate ) { + $flattened = $candidate; + break; + } + } + if ( null === $flattened ) { + $flattened = 'string'; + } + $schema['type'] = $flattened; + } + if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { if ( empty( $schema['properties'] ) ) { $schema['properties'] = new \stdClass(); diff --git a/tests/SanitizeSchemaUnionItemsTypeTest.php b/tests/SanitizeSchemaUnionItemsTypeTest.php new file mode 100644 index 0000000..0a57ff2 --- /dev/null +++ b/tests/SanitizeSchemaUnionItemsTypeTest.php @@ -0,0 +1,82 @@ + 'array', + 'items' => array( 'type' => array( 'integer', 'string' ) ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'string', $out['items']['type'] ); + } + + public function test_flatten_top_level_union_type(): void { + $schema = array( + 'type' => array( 'integer', 'string' ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'string', $out['type'] ); + } + + public function test_flatten_skips_null_picks_first_non_null(): void { + $schema = array( + 'type' => array( 'null', 'integer', 'string' ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'integer', $out['type'] ); + } + + public function test_flatten_all_null_falls_back_to_string(): void { + $schema = array( + 'type' => array( 'null' ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'string', $out['type'] ); + } + + public function test_passthrough_string_type_unchanged(): void { + $schema = array( 'type' => 'integer' ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'integer', $out['type'] ); + } + + public function test_flatten_via_recursion_through_properties(): void { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'terms' => array( + 'type' => 'array', + 'items' => array( 'type' => array( 'integer', 'string' ) ), + ), + ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'string', $out['properties']['terms']['items']['type'] ); + } + + public function test_flatten_via_recursion_through_items(): void { + $schema = array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => array( 'integer', 'string' ) ), + ), + ), + ); + $out = EMCP_Tools_Schema_Compat::sanitize( $schema ); + $this->assertSame( 'string', $out['items']['properties']['id']['type'] ); + } +}