diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index a52753e3cb..0671dc1257 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -3458,6 +3458,21 @@ function format_to_post( $content ) { return $content; } +/** + * Formerly used to escape strings before searching the DB. It was poorly documented and never worked as described. + * + * @since 2.5.0 + * @deprecated 4.0.0 + * @deprecated Use wpdb::esc_like() + * + * @param string $text The text to be escaped. + * @return string text, safe for inclusion in LIKE query. + */ +function like_escape($text) { + _deprecated_function( __FUNCTION__, '4.0', 'wpdb::esc_like()' ); + return str_replace( array( "%", "_" ), array( "\\%", "\\_" ), $text ); +} + /** * Determines if the URL can be accessed over SSL. * diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 56a9c1edd0..9292ee1762 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3099,18 +3099,6 @@ function tag_escape($tag_name) { return apply_filters( 'tag_escape', $safe_tag, $tag_name ); } -/** - * Escapes text for SQL LIKE special characters % and _. - * - * @since 2.5.0 - * - * @param string $text The text to be escaped. - * @return string text, safe for inclusion in LIKE query. - */ -function like_escape($text) { - return str_replace(array("%", "_"), array("\\%", "\\_"), $text); -} - /** * Convert full URL paths to absolute paths. * diff --git a/src/wp-includes/wp-db.php b/src/wp-includes/wp-db.php index 68ac32c219..7a09d62a9e 100644 --- a/src/wp-includes/wp-db.php +++ b/src/wp-includes/wp-db.php @@ -1168,6 +1168,29 @@ class wpdb { return @vsprintf( $query, $args ); } + /** + * First half of escaping for LIKE special characters % and _ before preparing for MySQL. + * + * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. + * + * Example Prepared Statement: + * $wild = '%'; + * $find = 'only 43% of planets'; + * $like = $wild . $wpdb->esc_like( $find ) . $wild; + * $sql = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_content LIKE %s", $like ); + * + * Example Escape Chain: + * $sql = esc_sql( $wpdb->esc_like( $input ) ); + * + * @since 4.0.0 + * + * @param string $text The raw text to be escaped. The input typed by the user should have no extra or deleted slashes. + * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call prepare or real_escape next. + */ + function esc_like( $text ) { + return addcslashes( $text, '_%\\' ); + } + /** * Print SQL/DB error. * diff --git a/tests/phpunit/tests/db.php b/tests/phpunit/tests/db.php index b019bc9592..39a1f11339 100644 --- a/tests/phpunit/tests/db.php +++ b/tests/phpunit/tests/db.php @@ -108,6 +108,105 @@ class Tests_DB extends WP_UnitTestCase { } } + /** + * @ticket 10041 + */ + function test_esc_like() { + global $wpdb; + + $inputs = array( + 'howdy%', //Single Percent + 'howdy_', //Single Underscore + 'howdy\\', //Single slash + 'howdy\\howdy%howdy_', //The works + 'howdy\'"[[]*#[^howdy]!+)(*&$#@!~|}{=--`/.,<>?', //Plain text + ); + $expected = array( + 'howdy\\%', + 'howdy\\_', + 'howdy\\\\', + 'howdy\\\\howdy\\%howdy\\_', + 'howdy\'"[[]*#[^howdy]!+)(*&$#@!~|}{=--`/.,<>?', + ); + + foreach ($inputs as $key => $input) { + $this->assertEquals($expected[$key], $wpdb->esc_like($input)); + } + } + + /** + * Test LIKE Queries + * + * Make sure $wpdb is fully compatible with esc_like() by testing the identity of various strings. + * When escaped properly, a string literal is always LIKE itself (1) + * and never LIKE any other string literal (0) no matter how crazy the SQL looks. + * + * @ticket 10041 + * @dataProvider data_like_query + * @param $data string The haystack, raw. + * @param $like string The like phrase, raw. + * @param $result string The expected comparison result; '1' = true, '0' = false + */ + function test_like_query( $data, $like, $result ) { + global $wpdb; + return $this->assertEquals( $result, $wpdb->get_var( $wpdb->prepare( "SELECT %s LIKE %s", $data, $wpdb->esc_like( $like ) ) ) ); + } + + function data_like_query() { + return array( + array( + 'aaa', + 'aaa', + '1', + ), + array( + 'a\\aa', // SELECT 'a\\aa' # This represents a\aa in both languages. + 'a\\aa', // LIKE 'a\\\\aa' + '1', + ), + array( + 'a%aa', + 'a%aa', + '1', + ), + array( + 'aaaa', + 'a%aa', + '0', + ), + array( + 'a\\%aa', // SELECT 'a\\%aa' + 'a\\%aa', // LIKE 'a\\\\\\%aa' # The PHP literal would be "LIKE 'a\\\\\\\\\\\\%aa'". This is why we need reliable escape functions! + '1', + ), + array( + 'a%aa', + 'a\\%aa', + '0', + ), + array( + 'a\\%aa', + 'a%aa', + '0', + ), + array( + 'a_aa', + 'a_aa', + '1', + ), + array( + 'aaaa', + 'a_aa', + '0', + ), + array( + 'howdy\'"[[]*#[^howdy]!+)(*&$#@!~|}{=--`/.,<>?', + 'howdy\'"[[]*#[^howdy]!+)(*&$#@!~|}{=--`/.,<>?', + '1', + ), + ); + } + /** * @ticket 18510 */ diff --git a/tests/phpunit/tests/formatting/LikeEscape.php b/tests/phpunit/tests/formatting/LikeEscape.php index 7027bc7e40..c72bbadc2b 100644 --- a/tests/phpunit/tests/formatting/LikeEscape.php +++ b/tests/phpunit/tests/formatting/LikeEscape.php @@ -6,6 +6,7 @@ class Tests_Formatting_LikeEscape extends WP_UnitTestCase { /** * @ticket 10041 + * @expectedDeprecated like_escape */ function test_like_escape() {