PHP ldap_add function to escape ldap special characters in DN syntax - php

I'm trying to add some users to my Ldap DB but I get some errors (invalid dn syntax) when I use some special characters like ",.". I need a function that escape all characters. I try preg_quote but I get some errors in some cases.
Thanks in advance
Code:
$user = 'Test , Name S.L';
if(!(ldap_add($ds, "cn=" . $user . ",".LDAP_DN_BASE, $info))) {
include 'error_new_account.php';
}

EDIT Jan 2013: added support for escaping leading/trailing spaces in DN strings, per RFC 4514. Thanks to Eugenio for pointing out this issue.
EDIT 2014: I added this function to PHP 5.6. The code below is now a like-for-like drop-in replacement for earlier PHP versions.
if (!function_exists('ldap_escape')) {
define('LDAP_ESCAPE_FILTER', 0x01);
define('LDAP_ESCAPE_DN', 0x02);
/**
* #param string $subject The subject string
* #param string $ignore Set of characters to leave untouched
* #param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the
* set(s) of characters to escape.
* #return string
*/
function ldap_escape($subject, $ignore = '', $flags = 0)
{
static $charMaps = array(
LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
);
// Pre-process the char maps on first call
if (!isset($charMaps[0])) {
$charMaps[0] = array();
for ($i = 0; $i < 256; $i++) {
$charMaps[0][chr($i)] = sprintf('\\%02x', $i);;
}
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
$chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
$charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
}
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
$chr = $charMaps[LDAP_ESCAPE_DN][$i];
unset($charMaps[LDAP_ESCAPE_DN][$i]);
$charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
}
}
// Create the base char map to escape
$flags = (int)$flags;
$charMap = array();
if ($flags & LDAP_ESCAPE_FILTER) {
$charMap += $charMaps[LDAP_ESCAPE_FILTER];
}
if ($flags & LDAP_ESCAPE_DN) {
$charMap += $charMaps[LDAP_ESCAPE_DN];
}
if (!$charMap) {
$charMap = $charMaps[0];
}
// Remove any chars to ignore from the list
$ignore = (string)$ignore;
for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
unset($charMap[$ignore[$i]]);
}
// Do the main replacement
$result = strtr($subject, $charMap);
// Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
if ($flags & LDAP_ESCAPE_DN) {
if ($result[0] === ' ') {
$result = '\\20' . substr($result, 1);
}
if ($result[strlen($result) - 1] === ' ') {
$result = substr($result, 0, -1) . '\\20';
}
}
return $result;
}
}
So you would do:
$user = 'Test , Name S.L';
$cn = ldap_escape($user, '', LDAP_ESCAPE_DN);
if (!ldap_add($ds, "cn={$cn}," . LDAP_DN_BASE, $info)) {
include 'error_new_account.php';
}

PHP 5.6 Beta released ldap_escape() function recently and it is in effect, However, this version is not production ready at present, you can very use it for your development purposes as of now.

Just a heads up if your not on PHP 5.6 yet, you can mirror the exact PHP 5.6 function ldap_escape() using the methods I created below, keep in mind this is meant for use in a class. The above answer doesn't perform exactly like the ldap_escape function, as in it doesn't escape all characters into a hex string if no flags have been given, so this would be more suitable for a drop in replacement for earlier versions of PHP, in an object oriented way.
I've documented every line for an easier understanding on whats going on. Scroll down for output.
Methods (Compatible with PHP 5 or greater):
/**
* Escapes the inserted value for LDAP.
*
* #param string $value The value to escape
* #param string $ignore The characters to ignore
* #param int $flags The PHP flag to use
*
* #return bool|string
*/
public function escapeManual($value, $ignore = '*', $flags = 0)
{
/*
* If a flag was supplied, we'll send the value
* off to be escaped using the PHP flag values
* and return the result.
*/
if($flags) {
return $this->escapeWithFlags($value, $ignore, $flags);
}
// Convert ignore string into an array
$ignores = str_split($ignore);
// Convert the value to a hex string
$hex = bin2hex($value);
/*
* Separate the string, with the hex length of 2,
* and place a backslash on the end of each section
*/
$value = chunk_split($hex, 2, "\\");
/*
* We'll append a backslash at the front of the string
* and remove the ending backslash of the string
*/
$value = "\\" . substr($value, 0, -1);
// Go through each character to ignore
foreach($ignores as $charToIgnore)
{
// Convert the characterToIgnore to a hex
$hexed = bin2hex($charToIgnore);
// Replace the hexed variant with the original character
$value = str_replace("\\" . $hexed, $charToIgnore, $value);
}
// Finally we can return the escaped value
return $value;
}
/**
* Escapes the inserted value with flags. Supplying either 1
* or 2 into the flags parameter will escape only certain values
*
*
* #param string $value The value to escape
* #param string $ignore The characters to ignore
* #param int $flags The PHP flag to use
* #return bool|string
*/
public function escapeWithFlags($value, $ignore = '*', $flags = 0)
{
// Convert ignore string into an array
$ignores = str_split($ignore);
$escapeFilter = ['\\', '*', '(', ')'];
$escapeDn = ['\\', ',', '=', '+', '<', '>', ';', '"', '#'];
switch($flags)
{
case 1:
// Int 1 equals to LDAP_ESCAPE_FILTER
$escapes = $escapeFilter;
break;
case 2:
// Int 2 equals to LDAP_ESCAPE_DN
$escapes = $escapeDn;
break;
case 3:
// If both LDAP_ESCAPE_FILTER and LDAP_ESCAPE_DN are used
$escapes = array_merge($escapeFilter, $escapeDn);
break;
default:
// Customize your own default return value
return false;
}
foreach($escapes as $escape)
{
// Make sure the escaped value isn't inside the ignore array
if( ! in_array($escape, $ignores))
{
$hexed = chunk_split(bin2hex($escape), 2, "\\");
$hexed = "\\" . substr($hexed, 0, -1);
$value = str_replace($escape, $hexed, $value);
}
}
return $value;
}
Tests (be aware that LDAP_ESCAPE constants are only available in PHP 5.6):
// Value to escape
$value = 'testing=+<>"";:#()*\x00';
$php = ldap_escape($value, $ignore = '*');
$man = $this->escapeManual($value, $ignore = '*');
echo $php; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30
echo $man; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30
$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_DN);
$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_DN);
echo $php; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00
echo $man; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00
$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_FILTER);
$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_FILTER);
echo $php; // testing=+<>"";:#\28\29*\5cx00
echo $man; // testing=+<>"";:#\28\29*\5cx00
Github Gist link: https://gist.github.com/stevebauman/0db9b5daa414d60fc266

Those characters must escaped to be part of the data of a distinguished name or relative distinguished name. Escape the character (as in all LDAP) with a backslash 2 hex digit, such as \2a. Anything else would not be in compliance with the standards body documents. See RFC4514 for more specific information regarding the string representation of distinguished names.

Related

Encoding/Escaping JSON Control Characters

I'm using MariaDB's COLUMN_JSON() function. As this bug illustrates, the function properly escapes double quotes, but not other characters that should be encoded/escaped.
Here's a silly example query to demonstrate how the JSON column is created.
SELECT CONCAT('[', GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE(
'name', `name`,
'value', `value`
)) SEPARATOR ','), ']') AS `json`
FROM `settings`
If the name or value contain invalid JSON characters, json_decode will fail.
I've written a PHP function to escape/encode the value that comes from the query, but it seems like there should be a better way.
/**
* Makes sure the JSON values built by COLUMN_JSON() in MariaDB are safe for json_decode()
* Assumes that double quotes are already escaped
*
* #param string $mysql_json
* #return string
*/
public static function jsonEscape($mysql_json)
{
$rtn = '';
for ($i = 0; $i < strlen($mysql_json); ++$i) {
$char = $mysql_json[$i];
if (($char === '\\') && ($mysql_json[$i + 1] !== '"')) {
// escape a backslash, but leave escaped double quotes intact
$rtn .= '\\\\';
} elseif (($ord = ord($char)) && ($ord < 32)) {
// hex encode control characters (below ASCII 32)
$rtn .= '\\u' . str_pad(dechex($ord), 4, '0', STR_PAD_LEFT);
} else {
$rtn .= $char;
}
}
return $rtn;
}
Examine the string character-by-character like this doesn't perform well. Perhaps there's a string replacement or regular expression that would be more performant?
Based on a comment from Halcyon, I switched to a str_replace() solution, and it performs much better! The performance difference between trim(json_encode(13), '"') and '\\u' . str_pad(dechex(13), 4, '0', STR_PAD_LEFT) is just barely better, but it makes the intent more clear.
private static $json_replace_search;
private static $json_replace_replace;
/**
* Makes sure the JSON values built by GROUP_CONCAT() and COLUMN_JSON() in MariaDB are safe for json_decode()
* Assumes that double quotes are already escaped
*
* #param string $mysql_json
* #return string
*/
public static function jsonEscape($mysql_json)
{
if (is_null(self::$json_replace_search)) {
// initialize
self::$json_replace_search = [];
self::$json_replace_replace = [];
// set up all of the control characters (below ASCII 32)
for ($i = 0; $i < 32; ++$i) {
self::$json_replace_search[$i] = chr($i);
self::$json_replace_replace[$i] = trim(json_encode(self::$json_replace_search[$i]), '"');
}
}
// replace them
return str_replace(self::$json_replace_search, self::$json_replace_replace, $mysql_json);
}
/**
*
* #param string $mysql_json
* #return mixed
*/
public static function jsonDecode($mysql_json)
{
return json_decode(self::jsonEscape($mysql_json));
}

Replace all substring instances with a variable string

If you had the string
'Old string Old more string Old some more string'
and you wanted to get
'New1 string New2 more string New3 some more string'
how would you do it?
In other words, you need to replace all instances of 'Old' with variable string 'New'.$i. How can it be done?
An iterative solution that doesn't need regular expressions:
$str = 'Old string Old more string Old some more string';
$old = 'Old';
$new = 'New';
$i = 1;
$tmpOldStrLength = strlen($old);
while (($offset = strpos($str, $old, $offset)) !== false) {
$str = substr_replace($str, $new . ($i++), $offset, $tmpOldStrLength);
}
$offset in strpos() is just a little bit micro-optimization. I don't know, if it's worth it (in fact I don't even know, if it changes anything), but the idea is that we don't need to search for $old in the substring that is already processed.
See Demo
Old string Old more string Old some more string
New1 string New2 more string New3 some more string
Use preg_replace_callback.
$count = 0;
$str = preg_replace_callback(
'~Old~',
create_function('$matches', 'return "New".$count++;'),
$str
);
From the PHP manual on str_replace:
Replace all occurrences of the search string with the replacement string
mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )
search
The value being searched for, otherwise known as the needle. An array may be used to designate multiple needles.
replace
The replacement value that replaces found search values. An array may be used to designate multiple replacements.
subject
The string or array being searched and replaced on, otherwise known as the haystack.
If subject is an array, then the search and replace is performed with every entry of subject, and the return value is an array as well.
count
If passed, this will be set to the number of replacements performed.
Use:
$str = 'Old string Old more string Old some more string';
$i = 1;
while (preg_match('/Old/', $str)) {
$str = preg_replace('/Old/', 'New'.$i++, $str, 1);
}
echo $str,"\n";
Output:
New1 string New2 more string New3 some more string
I had some similar solution like KingCrunch's, but as he already answered it, I was wondering about a str_replace variant with a callback for replacements and came up with this (Demo):
$subject = array('OldOldOld', 'Old string Old more string Old some more string');
$search = array('Old', 'string');
$replace = array(
function($found, $count) {return 'New'.$count;},
function($found, $count) {static $c=0; return 'String'.(++$c);}
);
$replace = array();
print_r(str_ureplace($search, $replace, $subject));
/**
* str_ureplace
*
* str_replace like function with callback
*
* #param string|array search
* #param callback|array $replace
* #param string|array $subject
* #param int $replace_count
* #return string|array subject with replaces, FALSE on error.
*/
function str_ureplace($search, $replace, $subject, &$replace_count = null) {
$replace_count = 0;
// Validate input
$search = array_values((array) $search);
$searchCount = count($search);
if (!$searchCount) {
return $subject;
}
foreach($search as &$v) {
$v = (string) $v;
}
unset($v);
$replaceSingle = is_callable($replace);
$replace = $replaceSingle ? array($replace) : array_values((array) $replace);
foreach($replace as $index=>$callback) {
if (!is_callable($callback)) {
throw new Exception(sprintf('Unable to use %s (#%d) as a callback', gettype($callback), $index));
}
}
// Search and replace
$subjectIsString = is_string($subject);
$subject = (array) $subject;
foreach($subject as &$haystack) {
if (!is_string($haystack)) continue;
foreach($search as $key => $needle) {
if (!$len = strlen($needle))
continue;
$replaceSingle && $key = 0;
$pos = 0;
while(false !== $pos = strpos($haystack, $needle, $pos)) {
$replaceWith = isset($replace[$key]) ? call_user_func($replace[$key], $needle, ++$replace_count) : '';
$haystack = substr_replace($haystack, $replaceWith, $pos, $len);
}
}
}
unset($haystack);
return $subjectIsString ? reset($subject) : $subject;
}

PHP array to postgres array

Now php can't work directly wit Postgresql array. For example, php taking postgresql array like
'{"foo","bar"}'
I need simple php function to create multidimensional postgresql array from php array.
I think that experimental pg_convert() isn't optimal because it needs of extra data to form simple array string for database output, maybe I misunderstood the idea of this function.
For example, I need to convert
$from=array( array( "par_1_1","par_1_2" ), array( "array_2_1", "array_2_2" ) );
$to='{{"par_1_1","par_1_2"},{"par_2_1","par_2_2"}}';
Can I use array_walk_recursive() to convert the deepest elements of array?
Here's a simple function for converting a PHP array to PG array.
function to_pg_array($set) {
settype($set, 'array'); // can be called with a scalar or array
$result = array();
foreach ($set as $t) {
if (is_array($t)) {
$result[] = to_pg_array($t);
} else {
$t = str_replace('"', '\\"', $t); // escape double quote
if (! is_numeric($t)) // quote only non-numeric values
$t = '"' . $t . '"';
$result[] = $t;
}
}
return '{' . implode(",", $result) . '}'; // format
}
A little edit that uses pg_escape_string for quoting and that support PHP NULLS and booleans:
/**
* Converts a php array into a postgres array (also multidimensional)
*
* Each element is escaped using pg_escape_string, only string values
* are enclosed within single quotes, numeric values no; special
* elements as php nulls or booleans are literally converted, so the
* php NULL value is written literally 'NULL' and becomes a postgres
* NULL (the same thing is done with TRUE and FALSE values).
*
* Examples :
* VARCHAR VERY BASTARD ARRAY :
* $input = array('bla bla', 'ehi "hello"', 'abc, def', ' \'VERY\' "BASTARD,\'value"', NULL);
*
* to_pg_array($input) ==>> 'ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]'
*
* try to put this value in a query (you will get a valid result):
* select unnest(ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]::varchar[])
*
* NUMERIC ARRAY:
* $input = array(1, 2, 3, 8.5, null, 7.32);
* to_pg_array($input) ==>> 'ARRAY[1,2,3,8.5,NULL,7.32]'
* try: select unnest(ARRAY[1,2,3,8.5,NULL,7.32]::numeric[])
*
* BOOLEAN ARRAY:
* $input = array(false, true, true, null);
* to_pg_array($input) ==>> 'ARRAY[FALSE,TRUE,TRUE,NULL]'
* try: select unnest(ARRAY[FALSE,TRUE,TRUE,NULL]::boolean[])
*
* MULTIDIMENSIONAL ARRAY:
* $input = array(array('abc', 'def'), array('ghi', 'jkl'));
* to_pg_array($input) ==>> 'ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]'
* try: select ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]::varchar[][]
*
* EMPTY ARRAY (is different than null!!!):
* $input = array();
* to_pg_array($input) ==>> 'ARRAY[]'
* try: select unnest(ARRAY[]::varchar[])
*
* NULL VALUE :
* $input = NULL;
* to_pg_array($input) ==>> 'NULL'
* the functions returns a string='NULL' (literally 'NULL'), so putting it
* in the query, it becomes a postgres null value.
*
* If you pass a value that is not an array, the function returns a literal 'NULL'.
*
* You should put the result of this functions directly inside a query,
* without quoting or escaping it and you cannot use this result as parameter
* of a prepared statement.
*
* Example:
* $q = 'INSERT INTO foo (field1, field_array) VALUES ($1, ' . to_pg_array($php_array) . '::varchar[])';
* $params = array('scalar_parameter');
*
* It is recommended to write the array type (ex. varchar[], numeric[], ...)
* because if the array is empty or contains only null values, postgres
* can give an error (cannot determine type of an empty array...)
*
* The function returns only a syntactically well-formed array, it does not
* make any logical check, you should consider that postgres gives errors
* if you mix different types (ex. numeric and text) or different dimensions
* in a multidim array.
*
* #param array $set PHP array
*
* #return string Array in postgres syntax
*/
function to_pg_array($set) {
if (is_null($set) || !is_array($set)) {
return 'NULL';
}
// can be called with a scalar or array
settype($set, 'array');
$result = array();
foreach ($set as $t) {
// Element is array : recursion
if (is_array($t)) {
$result[] = to_pg_array($t);
}
else {
// PHP NULL
if (is_null($t)) {
$result[] = 'NULL';
}
// PHP TRUE::boolean
elseif (is_bool($t) && $t == TRUE) {
$result[] = 'TRUE';
}
// PHP FALSE::boolean
elseif (is_bool($t) && $t == FALSE) {
$result[] = 'FALSE';
}
// Other scalar value
else {
// Escape
$t = pg_escape_string($t);
// quote only non-numeric values
if (!is_numeric($t)) {
$t = '\'' . $t . '\'';
}
$result[] = $t;
}
}
}
return 'ARRAY[' . implode(",", $result) . ']'; // format
}
This is the same as mstefano80's answer, but more human-readable, universal and modern (at least for me):
<?php
class Sql
{
/**
* Convert PHP-array to SQL-array
* https://stackoverflow.com/questions/5631387/php-array-to-postgres-array
*
* #param array $data
* #return string
*/
public static function toArray(array $data, $escape = 'pg_escape_string')
{
$result = [];
foreach ($data as $element) {
if (is_array($element)) {
$result[] = static::toArray($element, $escape);
} elseif ($element === null) {
$result[] = 'NULL';
} elseif ($element === true) {
$result[] = 'TRUE';
} elseif ($element === false) {
$result[] = 'FALSE';
} elseif (is_numeric($element)) {
$result[] = $element;
} elseif (is_string($element)) {
$result[] = "'" . $escape($element) . "'";
} else {
throw new \InvalidArgumentException("Unsupported array item");
}
}
return sprintf('ARRAY[%s]', implode(',', $result));
}
}
Tests:
<?php
use Sql;
class SqlTest extends \PHPUnit_Framework_TestCase
{
public function testToArray()
{
$this->assertSame("ARRAY['foo','bar']", Sql::toArray(['foo', 'bar']));
$this->assertSame("ARRAY[1,2]", Sql::toArray([1, 2]));
$this->assertSame("ARRAY[1,2]", Sql::toArray(['1', '2']));
$this->assertSame("ARRAY['foo\\\"bar','bar\'foo']", Sql::toArray(['foo"bar', 'bar\'foo'], function($str){
return addslashes($str);
}));
$this->assertSame("ARRAY[ARRAY['foo\\\"bar'],ARRAY['bar\'foo']]", Sql::toArray([['foo"bar'], ['bar\'foo']], function($str){
return addslashes($str);
}));
}
}

PHP Like thing similar to MySQL Like, for if statement?

I want an if statement that uses same thingy like mysql something LIKE '%something%'
I want to build an if statement in php.
if ($something is like %$somethingother%)
Is it possible?
The reason for me asking this question is that I don't want to change the MySQL command, it's a long page with many stuff on it, I don't want to build a different function for this.
Let me know if this is possible, if possible then how to do it .
if ($something is like %$somethingother%)
Is it possible?
no.
I don't want to change the MySQL command, it's a long page with many stuff on it
Use some good editor, that supports regular expressions in find & replace, and turn it to something like:
if(stripos($something, $somethingother) !== FALSE){
}
I know, this question isn't actual but I've solved similar problem :)
My solution:
/**
* SQL Like operator in PHP.
* Returns TRUE if match else FALSE.
* #param string $pattern
* #param string $subject
* #return bool
*/
function like_match($pattern, $subject)
{
$pattern = str_replace('%', '.*', preg_quote($pattern, '/'));
return (bool) preg_match("/^{$pattern}$/i", $subject);
}
Examples:
like_match('%uc%','Lucy'); //TRUE
like_match('%cy', 'Lucy'); //TRUE
like_match('lu%', 'Lucy'); //TRUE
like_match('%lu', 'Lucy'); //FALSE
like_match('cy%', 'Lucy'); //FALSE
look on strstr function
Use this function which works same like SQL LIKE operator but it will return boolean value and you can make your own condition with one more if statement
function like($str, $searchTerm) {
$searchTerm = strtolower($searchTerm);
$str = strtolower($str);
$pos = strpos($str, $searchTerm);
if ($pos === false)
return false;
else
return true;
}
$found = like('Apple', 'app'); //returns true
$notFound = like('Apple', 'lep'); //returns false
if($found){
// This will execute only when the text is like the desired string
}
Use function, that search string in another string like: strstr, strpos, substr_count.
strpos() is not working for so i have to use this preg_match()
$a = 'How are you?';
if (preg_match('/\bare\b/', $a)) {
echo 'true';
}
like in this e.g i am matching with word "are"
hope for someone it will be helpful
But you will have to give lowercase string then it will work fine.
Example of strstr function:
$myString = "Hello, world!";
echo strstr( $myString, "wor" ); // Displays 'world!'
echo ( strstr( $myString, "xyz" ) ? "Yes" : "No" ); // Displays 'No'
If you have access to a MySQL server, send a query like this with MySQLi:
$SQL="select case when '$Value' like '$Pattern' then 'True' else 'False' end as Result";
$Result=$MySQLi->query($SQL)->fetch_all(MYSQLI_ASSOC)[0]['Result'];
Result will be a string containing True or False. Let PHP do what it's good for and use SQL for likes.
I came across this requirement recently and came up with this:
/**
* Removes the diacritical marks from a string.
*
* Diacritical marks: {#link https://unicode-table.com/blocks/combining-diacritical-marks/}
*
* #param string $string The string from which to strip the diacritical marks.
* #return string Stripped string.
*/
function stripDiacriticalMarks(string $string): string
{
return preg_replace('/[\x{0300}-\x{036f}]/u', '', \Normalizer::normalize($string , \Normalizer::FORM_KD));
}
/**
* Checks if the string $haystack is like $needle, $needle can contain '%' and '_'
* characters which will behave as if used in a SQL LIKE condition. Character escaping
* is supported with '\'.
*
* #param string $haystack The string to check if it is like $needle.
* #param string $needle The string used to check if $haystack is like it.
* #param bool $ai Whether to check likeness in an accent-insensitive manner.
* #param bool $ci Whether to check likeness in a case-insensitive manner.
* #return bool True if $haystack is like $needle, otherwise, false.
*/
function like(string $haystack, string $needle, bool $ai = true, bool $ci = true): bool
{
if ($ai) {
$haystack = stripDiacriticalMarks($haystack);
$needle = stripDiacriticalMarks($needle);
}
$needle = preg_quote($needle, '/');
$tokens = [];
$needleLength = strlen($needle);
for ($i = 0; $i < $needleLength;) {
if ($needle[$i] === '\\') {
$i += 2;
if ($i < $needleLength) {
if ($needle[$i] === '\\') {
$tokens[] = '\\\\';
$i += 2;
} else {
$tokens[] = $needle[$i];
++$i;
}
} else {
$tokens[] = '\\\\';
}
} else {
switch ($needle[$i]) {
case '_':
$tokens[] = '.';
break;
case '%':
$tokens[] = '.*';
break;
default:
$tokens[] = $needle[$i];
break;
}
++$i;
}
}
return preg_match('/^' . implode($tokens) . '$/u' . ($ci ? 'i' : ''), $haystack) === 1;
}
/**
* Escapes a string in a way that `UString::like` will match it as-is, thus '%' and '_'
* would match a literal '%' and '_' respectively (and not behave as in a SQL LIKE
* condition).
*
* #param string $str The string to escape.
* #return string The escaped string.
*/
function escapeLike(string $str): string
{
return strtr($str, ['\\' => '\\\\', '%' => '\%', '_' => '\_']);
}
The code above is unicode aware to be able to catch cases like:
like('Hello 🙃', 'Hello _'); // true
like('Hello 🙃', '_e%o__'); // true
like('asdfas \\🙃H\\\\%🙃É\\l\\_🙃\\l\\o asdfasf', '%' . escapeLike('\\🙃h\\\\%🙃e\\l\\_🙃\\l\\o') . '%'); // true
You can try all of this on https://3v4l.org/O9LX0
I think it's worth mentioning the str_contains() function available in PHP 8 which performs a case-sensitive check indicating whether a string is contained within another string, returning true or false.
Example taken from the documentation:
$string = 'The lazy fox jumped over the fence';
if (str_contains($string, 'lazy')) {
echo "The string 'lazy' was found in the string\n";
}
if (str_contains($string, 'Lazy')) {
echo 'The string "Lazy" was found in the string';
} else {
echo '"Lazy" was not found because the case does not match';
}
//The above will output:
//The string 'lazy' was found in the string
//"Lazy" was not found because the case does not match
See the full documentation here.
like_match() example is the best
this one witch SQL reqest is simple (I used it before), but works slowly then like_match() and exost database server resources when you iterate by array keys and every round hit db server with request usually not necessery. I made it faster ferst cutting / shrink array by pattern elements but regexp on array works always faster.
I like like_match() :)

mb_str_replace()... is slow. any alternatives?

I want to make sure some string replacement's I'm running are multi byte safe. I've found a few mb_str_replace functions around the net but they're slow. I'm talking 20% increase after passing maybe 500-900 bytes through it.
Any recommendations? I'm thinking about using preg_replace as it's native and compiled in so it might be faster. Any thoughts would be appreciated.
As said there, str_replace is safe to use in utf-8 contexts, as long as all parameters are utf-8 valid, because it won't be any ambiguous match between both multibyte encoded strings. If you check the validity of your input, then you have no need to look for a different function.
As encoding is a real challenge when there are inputs from everywhere (utf8 or others), I prefer using only multibyte-safe functions. For str_replace, I am using this one which is fast enough.
if (!function_exists('mb_str_replace'))
{
function mb_str_replace($search, $replace, $subject, &$count = 0)
{
if (!is_array($subject))
{
$searches = is_array($search) ? array_values($search) : array($search);
$replacements = is_array($replace) ? array_values($replace) : array($replace);
$replacements = array_pad($replacements, count($searches), '');
foreach ($searches as $key => $search)
{
$parts = mb_split(preg_quote($search), $subject);
$count += count($parts) - 1;
$subject = implode($replacements[$key], $parts);
}
}
else
{
foreach ($subject as $key => $value)
{
$subject[$key] = mb_str_replace($search, $replace, $value, $count);
}
}
return $subject;
}
}
Here's my implementation, based off Alain's answer:
/**
* Replace all occurrences of the search string with the replacement string. Multibyte safe.
*
* #param string|array $search The value being searched for, otherwise known as the needle. An array may be used to designate multiple needles.
* #param string|array $replace The replacement value that replaces found search values. An array may be used to designate multiple replacements.
* #param string|array $subject The string or array being searched and replaced on, otherwise known as the haystack.
* If subject is an array, then the search and replace is performed with every entry of subject, and the return value is an array as well.
* #param string $encoding The encoding parameter is the character encoding. If it is omitted, the internal character encoding value will be used.
* #param int $count If passed, this will be set to the number of replacements performed.
* #return array|string
*/
public static function mbReplace($search, $replace, $subject, $encoding = 'auto', &$count=0) {
if(!is_array($subject)) {
$searches = is_array($search) ? array_values($search) : [$search];
$replacements = is_array($replace) ? array_values($replace) : [$replace];
$replacements = array_pad($replacements, count($searches), '');
foreach($searches as $key => $search) {
$replace = $replacements[$key];
$search_len = mb_strlen($search, $encoding);
$sb = [];
while(($offset = mb_strpos($subject, $search, 0, $encoding)) !== false) {
$sb[] = mb_substr($subject, 0, $offset, $encoding);
$subject = mb_substr($subject, $offset + $search_len, null, $encoding);
++$count;
}
$sb[] = $subject;
$subject = implode($replace, $sb);
}
} else {
foreach($subject as $key => $value) {
$subject[$key] = self::mbReplace($search, $replace, $value, $encoding, $count);
}
}
return $subject;
}
His doesn't accept a character encoding, although I suppose you could set it via mb_regex_encoding.
My unit tests pass:
function testMbReplace() {
$this->assertSame('bbb',Str::mbReplace('a','b','aaa','auto',$count1));
$this->assertSame(3,$count1);
$this->assertSame('ccc',Str::mbReplace(['a','b'],['b','c'],'aaa','auto',$count2));
$this->assertSame(6,$count2);
$this->assertSame("\xbf\x5c\x27",Str::mbReplace("\x27","\x5c\x27","\xbf\x27",'iso-8859-1'));
$this->assertSame("\xbf\x27",Str::mbReplace("\x27","\x5c\x27","\xbf\x27",'gbk'));
}
Top rated note on http://php.net/manual/en/ref.mbstring.php#109937 says str_replace works for multibyte strings.

Categories