Util.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4: */
  3. /**
  4. * File containing the Net_LDAP2_Util interface class.
  5. *
  6. * PHP version 5
  7. *
  8. * @category Net
  9. * @package Net_LDAP2
  10. * @author Benedikt Hallinger <beni@php.net>
  11. * @copyright 2009 Benedikt Hallinger
  12. * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
  13. * @version SVN: $Id$
  14. * @link http://pear.php.net/package/Net_LDAP2/
  15. */
  16. /**
  17. * Includes
  18. */
  19. require_once 'PEAR.php';
  20. /**
  21. * Utility Class for Net_LDAP2
  22. *
  23. * This class servers some functionality to the other classes of Net_LDAP2 but most of
  24. * the methods can be used separately as well.
  25. *
  26. * @category Net
  27. * @package Net_LDAP2
  28. * @author Benedikt Hallinger <beni@php.net>
  29. * @license http://www.gnu.org/copyleft/lesser.html LGPL
  30. * @link http://pear.php.net/package/Net_LDAP22/
  31. */
  32. class Net_LDAP2_Util extends PEAR
  33. {
  34. /**
  35. * Constructor
  36. *
  37. * @access public
  38. */
  39. public function __construct()
  40. {
  41. // We do nothing here, since all methods can be called statically.
  42. // In Net_LDAP <= 0.7, we needed a instance of Util, because
  43. // it was possible to do utf8 encoding and decoding, but this
  44. // has been moved to the LDAP class. The constructor remains only
  45. // here to document the downward compatibility of creating an instance.
  46. }
  47. /**
  48. * Explodes the given DN into its elements
  49. *
  50. * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a Distinguished Name is a sequence
  51. * of Relative Distinguished Names (RDNs), which themselves
  52. * are sets of Attributes. For each RDN a array is constructed where the RDN part is stored.
  53. *
  54. * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded to:
  55. * <kbd>array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' )</kbd>
  56. *
  57. * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of the BER encoding of
  58. * the X.500 AttributeValue rather than some LDAP string syntax. These values are hex-encoded
  59. * and prefixed with a #. To distinguish such BER values, ldap_explode_dn uses references to
  60. * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to:
  61. * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ];
  62. * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more information on BER.
  63. *
  64. * It also performs the following operations on the given DN:
  65. * - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", " ", or a hexpair
  66. * and strings beginning with "#".
  67. * - Removes the leading 'OID.' characters if the type is an OID instead of a name.
  68. * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
  69. *
  70. * OPTIONS is a list of name/value pairs, valid options are:
  71. * casefold Controls case folding of attribute types names.
  72. * Attribute values are not affected by this option.
  73. * The default is to uppercase. Valid values are:
  74. * lower Lowercase attribute types names.
  75. * upper Uppercase attribute type names. This is the default.
  76. * none Do not change attribute type names.
  77. * reverse If TRUE, the RDN sequence is reversed.
  78. * onlyvalues If TRUE, then only attributes values are returned ('foo' instead of 'cn=foo')
  79. *
  80. * @param string $dn The DN that should be exploded
  81. * @param array $options Options to use
  82. *
  83. * @static
  84. * @return array Parts of the exploded DN
  85. * @todo implement BER
  86. */
  87. public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper'))
  88. {
  89. if (!isset($options['onlyvalues'])) $options['onlyvalues'] = false;
  90. if (!isset($options['reverse'])) $options['reverse'] = false;
  91. if (!isset($options['casefold'])) $options['casefold'] = 'upper';
  92. // Escaping of DN and stripping of "OID."
  93. $dn = self::canonical_dn($dn, array('casefold' => $options['casefold']));
  94. // splitting the DN
  95. $dn_array = preg_split('/(?<=[^\\\\]),/', $dn);
  96. // clear wrong splitting (possibly we have split too much)
  97. // /!\ Not clear, if this is neccessary here
  98. //$dn_array = self::correct_dn_splitting($dn_array, ',');
  99. // construct subarrays for multivalued RDNs and unescape DN value
  100. // also convert to output format and apply casefolding
  101. foreach ($dn_array as $key => $value) {
  102. $value_u = self::unescape_dn_value($value);
  103. $rdns = self::split_rdn_multival($value_u[0]);
  104. if (count($rdns) > 1) {
  105. // MV RDN!
  106. foreach ($rdns as $subrdn_k => $subrdn_v) {
  107. // Casefolding
  108. if ($options['casefold'] == 'upper') {
  109. $subrdn_v = preg_replace_callback(
  110. "/^\w+=/",
  111. function ($matches) {
  112. return strtoupper($matches[0]);
  113. },
  114. $subrdn_v
  115. );
  116. } else if ($options['casefold'] == 'lower') {
  117. $subrdn_v = preg_replace_callback(
  118. "/^\w+=/",
  119. function ($matches) {
  120. return strtolower($matches[0]);
  121. },
  122. $subrdn_v
  123. );
  124. }
  125. if ($options['onlyvalues']) {
  126. preg_match('/(.+?)(?<!\\\\)=(.+)/', $subrdn_v, $matches);
  127. $rdn_ocl = $matches[1];
  128. $rdn_val = $matches[2];
  129. $unescaped = self::unescape_dn_value($rdn_val);
  130. $rdns[$subrdn_k] = $unescaped[0];
  131. } else {
  132. $unescaped = self::unescape_dn_value($subrdn_v);
  133. $rdns[$subrdn_k] = $unescaped[0];
  134. }
  135. }
  136. $dn_array[$key] = $rdns;
  137. } else {
  138. // normal RDN
  139. // Casefolding
  140. if ($options['casefold'] == 'upper') {
  141. $value = preg_replace_callback(
  142. "/^\w+=/",
  143. function ($matches) {
  144. return strtoupper($matches[0]);
  145. },
  146. $value
  147. );
  148. } else if ($options['casefold'] == 'lower') {
  149. $value = preg_replace_callback(
  150. "/^\w+=/",
  151. function ($matches) {
  152. return strtolower($matches[0]);
  153. },
  154. $value
  155. );
  156. }
  157. if ($options['onlyvalues']) {
  158. preg_match('/(.+?)(?<!\\\\)=(.+)/', $value, $matches);
  159. $dn_ocl = $matches[1];
  160. $dn_val = $matches[2];
  161. $unescaped = self::unescape_dn_value($dn_val);
  162. $dn_array[$key] = $unescaped[0];
  163. } else {
  164. $unescaped = self::unescape_dn_value($value);
  165. $dn_array[$key] = $unescaped[0];
  166. }
  167. }
  168. }
  169. if ($options['reverse']) {
  170. return array_reverse($dn_array);
  171. } else {
  172. return $dn_array;
  173. }
  174. }
  175. /**
  176. * Escapes a DN value according to RFC 2253
  177. *
  178. * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs.
  179. * The characters ",", "+", """, "\", "<", ">", ";", "#", "=" with a special meaning in RFC 2252
  180. * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair.
  181. * Finally all leading and trailing spaces are converted to sequences of \20.
  182. *
  183. * @param array $values An array containing the DN values that should be escaped
  184. *
  185. * @static
  186. * @return array The array $values, but escaped
  187. */
  188. public static function escape_dn_value($values = array())
  189. {
  190. // Parameter validation
  191. if (!is_array($values)) {
  192. $values = array($values);
  193. }
  194. foreach ($values as $key => $val) {
  195. // Escaping of filter meta characters
  196. $val = str_replace('\\', '\\\\', $val);
  197. $val = str_replace(',', '\,', $val);
  198. $val = str_replace('+', '\+', $val);
  199. $val = str_replace('"', '\"', $val);
  200. $val = str_replace('<', '\<', $val);
  201. $val = str_replace('>', '\>', $val);
  202. $val = str_replace(';', '\;', $val);
  203. $val = str_replace('#', '\#', $val);
  204. $val = str_replace('=', '\=', $val);
  205. // ASCII < 32 escaping
  206. $val = self::asc2hex32($val);
  207. // Convert all leading and trailing spaces to sequences of \20.
  208. if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) {
  209. $val = $matches[2];
  210. for ($i = 0; $i < strlen($matches[1]); $i++) {
  211. $val = '\20'.$val;
  212. }
  213. for ($i = 0; $i < strlen($matches[3]); $i++) {
  214. $val = $val.'\20';
  215. }
  216. }
  217. if (null === $val) $val = '\0'; // apply escaped "null" if string is empty
  218. $values[$key] = $val;
  219. }
  220. return $values;
  221. }
  222. /**
  223. * Undoes the conversion done by escape_dn_value().
  224. *
  225. * Any escape sequence starting with a baskslash - hexpair or special character -
  226. * will be transformed back to the corresponding character.
  227. *
  228. * @param array $values Array of DN Values
  229. *
  230. * @return array Same as $values, but unescaped
  231. * @static
  232. */
  233. public static function unescape_dn_value($values = array())
  234. {
  235. // Parameter validation
  236. if (!is_array($values)) {
  237. $values = array($values);
  238. }
  239. foreach ($values as $key => $val) {
  240. // strip slashes from special chars
  241. $val = str_replace('\\\\', '\\', $val);
  242. $val = str_replace('\,', ',', $val);
  243. $val = str_replace('\+', '+', $val);
  244. $val = str_replace('\"', '"', $val);
  245. $val = str_replace('\<', '<', $val);
  246. $val = str_replace('\>', '>', $val);
  247. $val = str_replace('\;', ';', $val);
  248. $val = str_replace('\#', '#', $val);
  249. $val = str_replace('\=', '=', $val);
  250. // Translate hex code into ascii
  251. $values[$key] = self::hex2asc($val);
  252. }
  253. return $values;
  254. }
  255. /**
  256. * Returns the given DN in a canonical form
  257. *
  258. * Returns false if DN is not a valid Distinguished Name.
  259. * DN can either be a string or an array
  260. * as returned by ldap_explode_dn, which is useful when constructing a DN.
  261. * The DN array may have be indexed (each array value is a OCL=VALUE pair)
  262. * or associative (array key is OCL and value is VALUE).
  263. *
  264. * It performs the following operations on the given DN:
  265. * - Removes the leading 'OID.' characters if the type is an OID instead of a name.
  266. * - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", ";", "#", "="), slashes ("/"), and any other character where the ASCII code is < 32 as \hexpair.
  267. * - Converts all leading and trailing spaces in values to be \20.
  268. * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
  269. *
  270. * OPTIONS is a list of name/value pairs, valid options are:
  271. * casefold Controls case folding of attribute type names.
  272. * Attribute values are not affected by this option. The default is to uppercase.
  273. * Valid values are:
  274. * lower Lowercase attribute type names.
  275. * upper Uppercase attribute type names. This is the default.
  276. * none Do not change attribute type names.
  277. * [NOT IMPLEMENTED] mbcescape If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}.
  278. * reverse If TRUE, the RDN sequence is reversed.
  279. * separator Separator to use between RDNs. Defaults to comma (',').
  280. *
  281. * Note: The empty string "" is a valid DN, so be sure not to do a "$can_dn == false" test,
  282. * because an empty string evaluates to false. Use the "===" operator instead.
  283. *
  284. * @param array|string $dn The DN
  285. * @param array $options Options to use
  286. *
  287. * @static
  288. * @return false|string The canonical DN or FALSE
  289. * @todo implement option mbcescape
  290. */
  291. public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ','))
  292. {
  293. if ($dn === '') return $dn; // empty DN is valid!
  294. // options check
  295. if (!isset($options['reverse'])) {
  296. $options['reverse'] = false;
  297. } else {
  298. $options['reverse'] = true;
  299. }
  300. if (!isset($options['casefold'])) $options['casefold'] = 'upper';
  301. if (!isset($options['separator'])) $options['separator'] = ',';
  302. if (!is_array($dn)) {
  303. // It is not clear to me if the perl implementation splits by the user defined
  304. // separator or if it just uses this separator to construct the new DN
  305. $dn = preg_split('/(?<=[^\\\\])'.$options['separator'].'/', $dn);
  306. // clear wrong splitting (possibly we have split too much)
  307. $dn = self::correct_dn_splitting($dn, $options['separator']);
  308. } else {
  309. // Is array, check, if the array is indexed or associative
  310. $assoc = false;
  311. foreach ($dn as $dn_key => $dn_part) {
  312. if (!is_int($dn_key)) {
  313. $assoc = true;
  314. }
  315. }
  316. // convert to indexed, if associative array detected
  317. if ($assoc) {
  318. $newdn = array();
  319. foreach ($dn as $dn_key => $dn_part) {
  320. if (is_array($dn_part)) {
  321. ksort($dn_part, SORT_STRING); // we assume here, that the rdn parts are also associative
  322. $newdn[] = $dn_part; // copy array as-is, so we can resolve it later
  323. } else {
  324. $newdn[] = $dn_key.'='.$dn_part;
  325. }
  326. }
  327. $dn =& $newdn;
  328. }
  329. }
  330. // Escaping and casefolding
  331. foreach ($dn as $pos => $dnval) {
  332. if (is_array($dnval)) {
  333. // subarray detected, this means very surely, that we had
  334. // a multivalued dn part, which must be resolved
  335. $dnval_new = '';
  336. foreach ($dnval as $subkey => $subval) {
  337. // build RDN part
  338. if (!is_int($subkey)) {
  339. $subval = $subkey.'='.$subval;
  340. }
  341. $subval_processed = self::canonical_dn($subval);
  342. if (false === $subval_processed) return false;
  343. $dnval_new .= $subval_processed.'+';
  344. }
  345. $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus
  346. } else {
  347. // try to split multivalued RDNS into array
  348. $rdns = self::split_rdn_multival($dnval);
  349. if (count($rdns) > 1) {
  350. // Multivalued RDN was detected!
  351. // The RDN value is expected to be correctly split by split_rdn_multival().
  352. // It's time to sort the RDN and build the DN!
  353. $rdn_string = '';
  354. sort($rdns, SORT_STRING); // Sort RDN keys alphabetically
  355. foreach ($rdns as $rdn) {
  356. $subval_processed = self::canonical_dn($rdn);
  357. if (false === $subval_processed) return false;
  358. $rdn_string .= $subval_processed.'+';
  359. }
  360. $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus
  361. } else {
  362. // no multivalued RDN!
  363. // split at first unescaped "="
  364. $dn_comp = preg_split('/(?<=[^\\\\])=/', $rdns[0], 2);
  365. $ocl = ltrim($dn_comp[0]); // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma)
  366. $val = $dn_comp[1];
  367. // strip 'OID.', otherwise apply casefolding and escaping
  368. if (substr(strtolower($ocl), 0, 4) == 'oid.') {
  369. $ocl = substr($ocl, 4);
  370. } else {
  371. if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl);
  372. if ($options['casefold'] == 'lower') $ocl = strtolower($ocl);
  373. $ocl = self::escape_dn_value(array($ocl));
  374. $ocl = $ocl[0];
  375. }
  376. // escaping of dn-value
  377. $val = self::escape_dn_value(array($val));
  378. $val = str_replace('/', '\/', $val[0]);
  379. $dn[$pos] = $ocl.'='.$val;
  380. }
  381. }
  382. }
  383. if ($options['reverse']) $dn = array_reverse($dn);
  384. return implode($options['separator'], $dn);
  385. }
  386. /**
  387. * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
  388. *
  389. * Any control characters with an ACII code < 32 as well as the characters with special meaning in
  390. * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
  391. * backslash followed by two hex digits representing the hexadecimal value of the character.
  392. *
  393. * @param array $values Array of values to escape
  394. *
  395. * @static
  396. * @return array Array $values, but escaped
  397. */
  398. public static function escape_filter_value($values = array())
  399. {
  400. // Parameter validation
  401. if (!is_array($values)) {
  402. $values = array($values);
  403. }
  404. foreach ($values as $key => $val) {
  405. // Escaping of filter meta characters
  406. $val = str_replace('\\', '\5c', $val);
  407. $val = str_replace('*', '\2a', $val);
  408. $val = str_replace('(', '\28', $val);
  409. $val = str_replace(')', '\29', $val);
  410. // ASCII < 32 escaping
  411. $val = self::asc2hex32($val);
  412. if (null === $val) $val = '\0'; // apply escaped "null" if string is empty
  413. $values[$key] = $val;
  414. }
  415. return $values;
  416. }
  417. /**
  418. * Undoes the conversion done by {@link escape_filter_value()}.
  419. *
  420. * Converts any sequences of a backslash followed by two hex digits into the corresponding character.
  421. *
  422. * @param array $values Array of values to escape
  423. *
  424. * @static
  425. * @return array Array $values, but unescaped
  426. */
  427. public static function unescape_filter_value($values = array())
  428. {
  429. // Parameter validation
  430. if (!is_array($values)) {
  431. $values = array($values);
  432. }
  433. foreach ($values as $key => $value) {
  434. // Translate hex code into ascii
  435. $values[$key] = self::hex2asc($value);
  436. }
  437. return $values;
  438. }
  439. /**
  440. * Converts all ASCII chars < 32 to "\HEX"
  441. *
  442. * @param string $string String to convert
  443. *
  444. * @static
  445. * @return string
  446. */
  447. public static function asc2hex32($string)
  448. {
  449. for ($i = 0; $i < strlen($string); $i++) {
  450. $char = substr($string, $i, 1);
  451. if (ord($char) < 32) {
  452. $hex = dechex(ord($char));
  453. if (strlen($hex) == 1) $hex = '0'.$hex;
  454. $string = str_replace($char, '\\'.$hex, $string);
  455. }
  456. }
  457. return $string;
  458. }
  459. /**
  460. * Converts all Hex expressions ("\HEX") to their original ASCII characters
  461. *
  462. * @param string $string String to convert
  463. *
  464. * @static
  465. * @author beni@php.net, heavily based on work from DavidSmith@byu.net
  466. * @return string
  467. */
  468. public static function hex2asc($string)
  469. {
  470. $string = preg_replace_callback(
  471. "/\\\[0-9A-Fa-f]{2}/",
  472. function ($matches) {
  473. return chr(hexdec($matches[0]));
  474. },
  475. $string
  476. );
  477. return $string;
  478. }
  479. /**
  480. * Split an multivalued RDN value into an Array
  481. *
  482. * A RDN can contain multiple values, spearated by a plus sign.
  483. * This function returns each separate ocl=value pair of the RDN part.
  484. *
  485. * If no multivalued RDN is detected, an array containing only
  486. * the original rdn part is returned.
  487. *
  488. * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to:
  489. * <kbd>array([0] => 'OU=Sales', [1] => 'CN=J. Smith')</kbd>
  490. *
  491. * The method trys to be smart if it encounters unescaped "+" characters, but may fail,
  492. * so ensure escaped "+"es in attr names and attr values.
  493. *
  494. * [BUG] If you have a multivalued RDN with unescaped plus characters
  495. * and there is a unescaped plus sign at the end of an value followed by an
  496. * attribute name containing an unescaped plus, then you will get wrong splitting:
  497. * $rdn = 'OU=Sales+C+N=J. Smith';
  498. * returns:
  499. * array('OU=Sales+C', 'N=J. Smith');
  500. * The "C+" is treaten as value of the first pair instead as attr name of the second pair.
  501. * To prevent this, escape correctly.
  502. *
  503. * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar)
  504. *
  505. * @static
  506. * @return array Array with the components of the multivalued RDN or Error
  507. */
  508. public static function split_rdn_multival($rdn)
  509. {
  510. $rdns = preg_split('/(?<!\\\\)\+/', $rdn);
  511. $rdns = self::correct_dn_splitting($rdns, '+');
  512. return array_values($rdns);
  513. }
  514. /**
  515. * Splits an attribute=value syntax into an array
  516. *
  517. * If escaped delimeters are used, they are returned escaped as well.
  518. * The split will occur at the first unescaped delimeter character.
  519. * In case an invalid delimeter is given, no split will be performed and an
  520. * one element array gets returned.
  521. * Optional also filter-assertion delimeters can be considered (>, <, >=, <=, ~=).
  522. *
  523. * @param string $attr Attribute and Value Syntax ("foo=bar")
  524. * @param boolean $extended If set to true, also filter-assertion delimeter will be matched
  525. * @param boolean $withDelim If set to true, the return array contains the delimeter at index 1, putting the value to index 2
  526. *
  527. * @return array Indexed array: 0=attribute name, 1=attribute value OR ($withDelim=true): 0=attr, 1=delimeter, 2=value
  528. */
  529. public static function split_attribute_string($attr, $extended=false, $withDelim=false)
  530. {
  531. if ($withDelim) $withDelim = PREG_SPLIT_DELIM_CAPTURE;
  532. if (!$extended) {
  533. return preg_split('/(?<!\\\\)(=)/', $attr, 2, $withDelim);
  534. } else {
  535. return preg_split('/(?<!\\\\)(>=|<=|>|<|~=|=)/', $attr, 2, $withDelim);
  536. }
  537. }
  538. /**
  539. * Corrects splitting of dn parts
  540. *
  541. * @param array $dn Raw DN array
  542. * @param array $separator Separator that was used when splitting
  543. *
  544. * @return array Corrected array
  545. * @access protected
  546. */
  547. protected static function correct_dn_splitting($dn = array(), $separator = ',')
  548. {
  549. foreach ($dn as $key => $dn_value) {
  550. $dn_value = $dn[$key]; // refresh value (foreach caches!)
  551. // if the dn_value is not in attr=value format, then we had an
  552. // unescaped separator character inside the attr name or the value.
  553. // We assume, that it was the attribute value.
  554. // [TODO] To solve this, we might ask the schema. Keep in mind, that UTIL class
  555. // must remain independent from the other classes or connections.
  556. if (!preg_match('/.+(?<!\\\\)=.+/', $dn_value)) {
  557. unset($dn[$key]);
  558. if (array_key_exists($key-1, $dn)) {
  559. $dn[$key-1] = $dn[$key-1].$separator.$dn_value; // append to previous attr value
  560. } else {
  561. $dn[$key+1] = $dn_value.$separator.$dn[$key+1]; // first element: prepend to next attr name
  562. }
  563. }
  564. }
  565. return array_values($dn);
  566. }
  567. }
  568. ?>