diff --git a/includes/forms/form-taxonomy.php b/includes/forms/form-taxonomy.php index 9ee0f015..6f12758b 100644 --- a/includes/forms/form-taxonomy.php +++ b/includes/forms/form-taxonomy.php @@ -342,13 +342,9 @@ function delete_term( $term, $tt_id, $taxonomy, $deleted_term ) { global $wpdb; // vars - $search = $taxonomy . '_' . $term . '_%'; - $_search = '_' . $search; - - // escape '_' - // http://stackoverflow.com/questions/2300285/how-do-i-escape-in-sql-server - $search = str_replace( '_', '\_', $search ); - $_search = str_replace( '_', '\_', $_search ); + // esc_like() escapes %, _ and \ for the LIKE clause; append the trailing wildcard. + $search = $wpdb->esc_like( "{$taxonomy}_{$term}_" ) . '%'; + $_search = $wpdb->esc_like( "_{$taxonomy}_{$term}_" ) . '%'; // delete $result = $wpdb->query( diff --git a/includes/upgrades.php b/includes/upgrades.php index ee7ff0b8..1958064a 100644 --- a/includes/upgrades.php +++ b/includes/upgrades.php @@ -463,13 +463,9 @@ function acf_upgrade_550_taxonomy( $taxonomy ) { global $wpdb; // vars - $search = $taxonomy . '_%'; - $_search = '_' . $search; - - // escape '_' - // http://stackoverflow.com/questions/2300285/how-do-i-escape-in-sql-server - $search = str_replace( '_', '\_', $search ); - $_search = str_replace( '_', '\_', $_search ); + // esc_like() escapes %, _ and \ for the LIKE clause; append the trailing wildcard. + $search = $wpdb->esc_like( "{$taxonomy}_" ) . '%'; + $_search = $wpdb->esc_like( "_{$taxonomy}_" ) . '%'; // search // results show faster query times using 2 LIKE vs 2 wildcards diff --git a/tests/php/includes/test-like-escaping-parity.php b/tests/php/includes/test-like-escaping-parity.php new file mode 100644 index 00000000..dd33217f --- /dev/null +++ b/tests/php/includes/test-like-escaping-parity.php @@ -0,0 +1,134 @@ +esc_like(). These + * tests capture the generated SQL (the queries are no-ops under WorDBless) + * and assert the dynamic part is escaped, comparing against patterns built + * with the same $wpdb->prepare()/esc_like() primitives the source uses. + * + * @package wordpress/secure-custom-fields + */ + +use WorDBless\BaseTestCase; + +/** + * Tests that the surviving wp_options LIKE queries escape via esc_like(). + */ +class Test_Like_Escaping_Parity extends BaseTestCase { + + /** + * Captured wp_options LIKE queries. + * + * @var array + */ + protected $captured = array(); + + /** + * Start capturing queries. + */ + public function set_up() { + parent::set_up(); + $this->captured = array(); + add_filter( 'wordbless_wpdb_query_results', array( $this, 'capture_query' ), 10, 2 ); + } + + /** + * Stop capturing queries. + */ + public function tear_down() { + remove_filter( 'wordbless_wpdb_query_results', array( $this, 'capture_query' ), 10 ); + parent::tear_down(); + } + + /** + * Record any wp_options LIKE query. + * + * @param array $results The (empty) results array. + * @param string $query The SQL passed to wpdb::query(). + * @return array + */ + public function capture_query( $results, $query ) { + if ( false !== strpos( (string) $query, 'option_name LIKE' ) ) { + $this->captured[] = (string) $query; + } + return $results; + } + + /** + * The escaped fragment a correctly-escaped literal produces in the SQL. + * + * @param string $literal The literal portion of the LIKE pattern. + * @return string + */ + protected function expected_fragment( $literal ) { + global $wpdb; + return $wpdb->prepare( '%s', $wpdb->esc_like( $literal ) . '%' ); + } + + /** + * The most recent captured wp_options LIKE query. + * + * @return string + */ + protected function last_query() { + return empty( $this->captured ) ? '' : end( $this->captured ); + } + + /** + * Ensures acf_upgrade_550_taxonomy() escapes the taxonomy in its SELECT. + */ + public function test_upgrade_550_taxonomy_escapes_like() { + global $wpdb; + + // A taxonomy name carrying a % wildcard proves the escaping. + acf_upgrade_550_taxonomy( 'ta%x' ); + + $query = $this->last_query(); + $this->assertNotEmpty( $query, 'The upgrade SELECT should have been captured.' ); + $this->assertStringContainsString( + $this->expected_fragment( 'ta%x_' ), + $query, + 'The taxonomy should be escaped via esc_like().' + ); + + $bad = $wpdb->prepare( '%s', str_replace( '_', '\_', 'ta%x_%' ) ); + $this->assertStringNotContainsString( + $bad, + $query, + 'The unescaped (active-wildcard) pattern must not be generated.' + ); + } + + /** + * Ensures acf_form_taxonomy::delete_term() escapes the taxonomy and term + * in its legacy (no-termmeta) DELETE. + */ + public function test_delete_term_escapes_like() { + global $wpdb; + + // Force the legacy path: acf_isset_termmeta() returns false when the + // stored db_version predates termmeta (WP < 4.4 / db_version 34370). + update_option( 'db_version', 34000 ); + + $form = new acf_form_taxonomy(); + $form->delete_term( 7, 0, 'ta%x', null ); + + $query = $this->last_query(); + $this->assertNotEmpty( $query, 'The delete DELETE should have been captured.' ); + $this->assertStringContainsString( + $this->expected_fragment( 'ta%x_7_' ), + $query, + 'The taxonomy and term should be escaped via esc_like().' + ); + + $bad = $wpdb->prepare( '%s', str_replace( '_', '\_', 'ta%x_7_%' ) ); + $this->assertStringNotContainsString( + $bad, + $query, + 'The unescaped (active-wildcard) pattern must not be generated.' + ); + } +}