1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
16: class Turba_Driver_Ldap extends Turba_Driver
17: {
18: 19: 20: 21: 22:
23: protected $_ds = 0;
24:
25: 26: 27: 28: 29:
30: protected $_syntaxCache = array();
31:
32: 33: 34: 35: 36: 37: 38: 39:
40: public function __construct($name = '', array $params = array())
41: {
42: if (!Horde_Util::extensionExists('ldap')) {
43: throw new Turba_Exception(_("LDAP support is required but the LDAP module is not available or not loaded."));
44: }
45:
46: $params = array_merge(array(
47: 'charset' => '',
48: 'deref' => LDAP_DEREF_NEVER,
49: 'multiple_entry_separator' => ', ',
50: 'port' => 389,
51: 'root' => '',
52: 'scope' => 'sub',
53: 'server' => 'localhost'
54: ), $params);
55:
56: parent::__construct($name, $params);
57: }
58:
59: 60: 61: 62: 63: 64:
65: protected function _connect()
66: {
67: if ($this->_ds) {
68: return;
69: }
70:
71: if (!($this->_ds = @ldap_connect($this->_params['server'], $this->_params['port']))) {
72: throw new Turba_Exception(_("Connection failure"));
73: }
74:
75:
76: if (!empty($this->_params['version'])) {
77: @ldap_set_option($this->_ds, LDAP_OPT_PROTOCOL_VERSION, $this->_params['version']);
78: }
79:
80:
81: if (!empty($this->_params['deref'])) {
82: @ldap_set_option($this->_ds, LDAP_OPT_DEREF, $this->_params['deref']);
83: }
84:
85:
86: if (!empty($this->_params['referrals'])) {
87: @ldap_set_option($this->_ds, LDAP_OPT_REFERRALS, $this->_params['referrals']);
88: }
89:
90:
91: if (!empty($this->_params['tls']) &&
92: !@ldap_start_tls($this->_ds)) {
93: throw new Turba_Exception(sprintf(_("STARTTLS failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
94: }
95:
96:
97: if (isset($this->_params['bind_dn']) &&
98: isset($this->_params['bind_password'])) {
99: $error = !@ldap_bind($this->_ds, $this->_params['bind_dn'], $this->_params['bind_password']);
100: } else {
101: $error = !(@ldap_bind($this->_ds));
102: }
103:
104: if ($error) {
105: throw new Turba_Exception(sprintf(_("Bind failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
106: }
107: }
108:
109: 110: 111: 112: 113: 114: 115: 116:
117: public function toDriverKeys(array $hash)
118: {
119:
120: if (is_array($this->_params['dn'])) {
121: foreach ($this->_params['dn'] as $param) {
122: foreach ($this->map as $turbaname => $ldapname) {
123: if ((is_array($this->map[$turbaname])) &&
124: (isset($this->map[$turbaname]['attribute'])) &&
125: ($this->map[$turbaname]['attribute'] == $param)) {
126: $fieldarray = array();
127: foreach ($this->map[$turbaname]['fields'] as $mapfield) {
128: $fieldarray[] = isset($hash[$mapfield])
129: ? $hash[$mapfield]
130: : '';
131: }
132: $hash[$turbaname] = Turba::formatCompositeField($this->map[$turbaname]['format'], $fieldarray);
133: }
134: }
135: }
136: }
137:
138:
139: return parent::toDriverKeys($hash);
140: }
141:
142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153:
154: protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false)
155: {
156: $this->_connect();
157:
158:
159: $filter = '';
160: if (count($criteria)) {
161: foreach ($criteria as $key => $vals) {
162: if ($key == 'OR') {
163: $filter .= '(|' . $this->_buildSearchQuery($vals) . ')';
164: } elseif ($key == 'AND') {
165: $filter .= '(&' . $this->_buildSearchQuery($vals) . ')';
166: }
167: }
168: } elseif (!empty($this->_params['objectclass'])) {
169:
170: $filter = Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
171: }
172:
173:
174: if (!empty($this->_params['filter'])) {
175: $filter = '(&' . '(' . $this->_params['filter'] . ')' . $filter . ')';
176: }
177:
178: 179:
180: $attr = $fields;
181: if (!in_array('sn', $attr)) {
182: $attr[] = 'sn';
183: }
184:
185: 186: 187:
188: $sizelimit = 0;
189: if (!empty($this->_params['sizelimit'])) {
190: $sizelimit = $this->_params['sizelimit'];
191: }
192:
193:
194: Horde::logMessage(sprintf('LDAP query by Turba_Driver_ldap::_search(): user = %s, root = %s (%s); filter = "%s"; attributes = "%s"; deref = "%s" ; sizelimit = %d',
195: $GLOBALS['registry']->getAuth(), $this->_params['root'], $this->_params['server'], $filter, implode(', ', $attr), $this->_params['deref'], $sizelimit), 'DEBUG');
196:
197: 198:
199: $func = ($this->_params['scope'] == 'one')
200: ? 'ldap_list'
201: : 'ldap_search';
202:
203: if (!($res = @$func($this->_ds, $this->_params['root'], $filter, $attr, 0, $sizelimit))) {
204: throw new Turba_Exception(sprintf(_("Query failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
205: }
206:
207: return $count_only ? count($this->_getResults($fields, $res)) : $this->_getResults($fields, $res);
208: }
209:
210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221:
222: protected function _read($key, $ids, $owner, array $fields,
223: array $blobFields = array())
224: {
225:
226: if ($key != 'dn') {
227: return array();
228: }
229:
230: $this->_connect();
231:
232: if (empty($this->_params['objectclass'])) {
233: $filter = null;
234: } else {
235: $filter = (string)Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
236: }
237:
238: 239:
240: $attr = $fields;
241: if (!in_array('sn', $attr)) {
242: $attr[] = 'sn';
243: }
244:
245:
246: if (is_array($ids) && !empty($ids)) {
247: $results = array();
248: foreach ($ids as $d) {
249: $res = @ldap_read($this->_ds, Horde_String::convertCharset($d, 'UTF-8', $this->_params['charset']), $filter, $attr);
250: if ($res) {
251: $results = array_merge($results, $this->_getResults($fields, $res));
252: } else {
253: throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
254: }
255: }
256:
257: return $results;
258: }
259:
260: $res = @ldap_read($this->_ds, Horde_String::convertCharset($this->_params['root'], 'UTF-8', $this->_params['charset']), $filter, $attr);
261: if (!$res) {
262: throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
263: }
264:
265: return $this->_getResults($fields, $res);
266: }
267:
268: 269: 270: 271: 272: 273: 274: 275:
276: protected function _add(array $attributes, array $blob_fields = array())
277: {
278: if (empty($attributes['dn'])) {
279: throw new Turba_Exception('Tried to add an object with no dn: [' . serialize($attributes) . '].');
280: }
281: if (empty($this->_params['objectclass'])) {
282: throw new Turba_Exception('Tried to add an object with no objectclass: [' . serialize($attributes) . '].');
283: }
284:
285: $this->_connect();
286:
287:
288: $dn = $attributes['dn'];
289: unset($attributes['dn']);
290:
291:
292: if (!is_array($this->_params['objectclass'])) {
293: $attributes['objectclass'] = $this->_params['objectclass'];
294: } else {
295: $i = 0;
296: foreach ($this->_params['objectclass'] as $objectclass) {
297: $attributes['objectclass'][$i++] = $objectclass;
298: }
299: }
300:
301:
302: $attributes = array_filter($attributes, array($this, '_emptyAttributeFilter'));
303:
304: 305:
306: if (!empty($this->_params['checkrequired'])) {
307: $required = $this->_checkRequiredAttributes($this->_params['objectclass']);
308:
309: foreach ($required as $k => $v) {
310: if (!isset($attributes[$v])) {
311: $attributes[$v] = $this->_params['checkrequired_string'];
312: }
313: }
314: }
315:
316: $this->_encodeAttributes($attributes);
317:
318: if (!@ldap_add($this->_ds, Horde_String::convertCharset($dn, 'UTF-8', $this->_params['charset']), $attributes)) {
319: throw new Turba_Exception('Failed to add an object: [' . ldap_errno($this->_ds) . '] "' . ldap_error($this->_ds) . '" DN: ' . $dn . ' (attributes: [' . serialize($attributes) . '])');
320: }
321: }
322:
323: 324: 325: 326: 327:
328: protected function _canAdd()
329: {
330: return true;
331: }
332:
333: 334: 335: 336: 337: 338: 339: 340:
341: protected function _delete($object_key, $object_id)
342: {
343: if ($object_key != 'dn') {
344: throw new Turba_Exception(_("Invalid key specified."));
345: }
346:
347: $this->_connect();
348:
349: if (!@ldap_delete($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']))) {
350: throw new Turba_Exception(sprintf(_("Delete failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
351: }
352: }
353:
354: 355: 356: 357: 358: 359: 360: 361:
362: protected function _save(Turba_Object $object)
363: {
364: $this->_connect();
365:
366: list($object_key, $object_id) = each($this->toDriverKeys(array('__key' => $object->getValue('__key'))));
367: $attributes = $this->toDriverKeys($object->getAttributes());
368:
369: 370: 371:
372: if (empty($this->_params['objectclass'])) {
373: $filter = null;
374: } else {
375: $filter = (string)Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
376: }
377: $oldres = @ldap_read($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $filter, array_merge(array_keys($attributes), array('objectclass')));
378: $info = ldap_get_attributes($this->_ds, ldap_first_entry($this->_ds, $oldres));
379:
380: if ($this->_params['version'] == 3 &&
381: Horde_String::lower(str_replace(array(',', '"'), array('\\2C', ''), $this->_makeKey($attributes))) !=
382: Horde_String::lower(str_replace(',', '\\2C', $object_id))) {
383:
384: $newrdn = $this->_makeRDN($attributes);
385: if ($newrdn == '') {
386: throw new Turba_Exception(_("Missing DN in LDAP source configuration."));
387: }
388:
389: if (ldap_rename($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']),
390: Horde_String::convertCharset($newrdn, 'UTF-8', $this->_params['charset']), $this->_params['root'], true)) {
391: $object_id = $newrdn . ',' . $this->_params['root'];
392: } else {
393: throw new Turba_Exception(sprintf(_("Failed to change name: (%s) %s; Old DN = %s, New DN = %s, Root = %s"), ldap_errno($this->_ds), ldap_error($this->_ds), $object_id, $newrdn, $this->_params['root']));
394: }
395: }
396:
397:
398: $info = array_change_key_case($info, CASE_LOWER);
399: $attributes = array_change_key_case($attributes, CASE_LOWER);
400:
401: foreach ($info as $key => $value) {
402: $var = $info[$key];
403: $oldval = null;
404:
405: 406: 407:
408: if (isset($attributes[$key]) &&
409: ($var[0] != $attributes[$key]) &&
410: $attributes[$key] == '') {
411:
412: $oldval[$key] = $var[0];
413: if (!@ldap_mod_del($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $oldval)) {
414: throw new Turba_Exception(sprintf(_("Modify failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
415: }
416: unset($attributes[$key]);
417: } elseif (isset($attributes[$key]) &&
418: $var[0] == $attributes[$key]) {
419:
420: unset($attributes[$key]);
421: }
422: }
423:
424: unset($attributes[Horde_String::lower($object_key)]);
425: $this->_encodeAttributes($attributes);
426: $attributes = array_filter($attributes, array($this, '_emptyAttributeFilter'));
427:
428:
429: $oldClasses = array_map(array('Horde_String', 'lower'), $info['objectclass']);
430: array_shift($oldClasses);
431: $attributes['objectclass'] = array_unique(array_map('strtolower', array_merge($info['objectclass'], $this->_params['objectclass'])));
432: unset($attributes['objectclass']['count']);
433: $attributes['objectclass'] = array_values($attributes['objectclass']);
434:
435:
436: if ((!array_diff($oldClasses, $attributes['objectclass']))) {
437: unset($attributes['objectclass']);
438: }
439: if (!@ldap_modify($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $attributes)) {
440: throw new Turba_Exception(sprintf(_("Modify failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
441: }
442:
443: return $object_id;
444: }
445:
446: 447: 448: 449: 450: 451: 452: 453: 454:
455: protected function _makeRDN(array $attributes)
456: {
457: if (!is_array($this->_params['dn'])) {
458: return '';
459: }
460:
461: $pairs = array();
462: foreach ($this->_params['dn'] as $param) {
463: if (isset($attributes[$param])) {
464: $pairs[] = array($param, $attributes[$param]);
465: }
466: }
467:
468: return Horde_Ldap::quoteDN($pairs);
469: }
470:
471: 472: 473: 474: 475: 476: 477: 478: 479:
480: protected function _makeKey(array $attributes)
481: {
482: return $this->_makeRDN($attributes) . ',' . $this->_params['root'];
483: }
484:
485: 486: 487: 488: 489: 490: 491:
492: protected function _buildSearchQuery(array $criteria)
493: {
494: $clause = '';
495:
496: foreach ($criteria as $key => $vals) {
497: if (!empty($vals['OR'])) {
498: $clause .= '(|' . $this->_buildSearchQuery($vals) . ')';
499: } elseif (!empty($vals['AND'])) {
500: $clause .= '(&' . $this->_buildSearchQuery($vals) . ')';
501: } else {
502: if (isset($vals['field'])) {
503: $rhs = Horde_String::convertCharset($vals['test'], 'UTF-8', $this->_params['charset']);
504: $clause .= Horde_Ldap::buildClause($vals['field'], $vals['op'], $rhs, array('begin' => !empty($vals['begin'])));
505: } else {
506: foreach ($vals as $test) {
507: if (!empty($test['OR'])) {
508: $clause .= '(|' . $this->_buildSearchQuery($test) . ')';
509: } elseif (!empty($test['AND'])) {
510: $clause .= '(&' . $this->_buildSearchQuery($test) . ')';
511: } else {
512: $rhs = Horde_String::convertCharset($test['test'], 'UTF-8', $this->_params['charset']);
513: $clause .= Horde_Ldap::buildClause($test['field'], $test['op'], $rhs, array('begin' => !empty($vals['begin'])));
514: }
515: }
516: }
517: }
518: }
519:
520: return $clause;
521: }
522:
523: 524: 525: 526: 527: 528: 529: 530: 531:
532: protected function _getResults(array $fields, $res)
533: {
534: $entries = @ldap_get_entries($this->_ds, $res);
535: if ($entries === false) {
536: throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
537: }
538:
539:
540: $results = array();
541: for ($i = 0; $i < $entries['count']; ++$i) {
542: $entry = $entries[$i];
543: $result = array();
544:
545: foreach ($fields as $field) {
546: $field_l = Horde_String::lower($field);
547: if ($field == 'dn') {
548: $result[$field] = Horde_String::convertCharset($entry[$field_l], $this->_params['charset'], 'UTF-8');
549: } else {
550: $result[$field] = '';
551: if (!empty($entry[$field_l])) {
552: for ($j = 0; $j < $entry[$field_l]['count']; $j++) {
553: if (!empty($result[$field])) {
554: $result[$field] .= $this->_params['multiple_entry_separator'];
555: }
556: $result[$field] .= Horde_String::convertCharset($entry[$field_l][$j], $this->_params['charset'], 'UTF-8');
557: }
558:
559: 560:
561: if (!empty($this->_params['checksyntax'])) {
562: $postal = $this->_isPostalAddress($field_l);
563: } else {
564: 565:
566: $attr = array_search($field_l, $this->map);
567: $postal = (!empty($attr) && !empty($GLOBALS['attributes'][$attr]) &&
568: $GLOBALS['attributes'][$attr]['type'] == 'address');
569: }
570: if ($postal) {
571: $result[$field] = str_replace('$', "\r\n", $result[$field]);
572: }
573: }
574: }
575: }
576:
577: $results[] = $result;
578: }
579:
580: return $results;
581: }
582:
583: 584: 585: 586: 587: 588: 589:
590: protected function _emptyAttributeFilter($var)
591: {
592: if (!is_array($var)) {
593: return ($var != '');
594: }
595:
596: if (!count($var)) {
597: return false;
598: }
599:
600: foreach ($var as $v) {
601: if ($v == '') {
602: return false;
603: }
604: }
605:
606: return true;
607: }
608:
609: 610: 611: 612: 613: 614:
615: protected function _encodeAttributes(&$attributes)
616: {
617: foreach ($attributes as $key => $val) {
618:
619: if (!empty($this->_params['checksyntax'])) {
620: $postal = $this->_isPostalAddress($key);
621: } else {
622: 623:
624: $attr = array_search($key, $this->map);
625: $postal = (!empty($attr) && !empty($val) && !empty($GLOBALS['attributes'][$attr]) &&
626: $GLOBALS['attributes'][$attr]['type'] == 'address');
627: }
628: if ($postal) {
629:
630: $val = str_replace(array("\r\n", "\r", "\n"), '$', $val);
631: }
632:
633: if (!is_array($val)) {
634: $attributes[$key] = Horde_String::convertCharset($val, 'UTF-8', $this->_params['charset']);
635: }
636: }
637: }
638:
639: 640: 641: 642: 643: 644: 645: 646: 647: 648:
649: protected function _checkRequiredAttributes(array $objectclasses)
650: {
651: $ldap = new Horde_Ldap($this->_convertParameters($this->_params));
652: $schema = $ldap->schema();
653:
654: $retval = array();
655: foreach ($objectclasses as $oc) {
656: if (Horde_String::lower($oc) == 'top') {
657: continue;
658: }
659:
660: $required = $schema->must($oc, true);
661: if (is_array($required)) {
662: foreach ($required as $v) {
663: if ($this->_isString($v)) {
664: $retval[] = Horde_String::lower($v);
665: }
666: }
667: }
668: }
669:
670: return $retval;
671: }
672:
673: 674: 675: 676: 677: 678: 679:
680: protected function _isString($attribute)
681: {
682: $syntax = $this->_getSyntax($attribute);
683:
684: 685: 686: 687:
688: $okSyntax = array(
689: 44 => 1,
690: 41 => 1,
691: 39 => 1,
692: 34 => 1,
693: 26 => 1,
694: 15 => 1,
695: );
696:
697: return (preg_match('/^(.*)\.(\d+)\{\d+\}$/', $syntax, $matches) &&
698: ($matches[1] == "1.3.6.1.4.1.1466.115.121.1") &&
699: isset($okSyntax[$matches[2]]));
700: }
701:
702: 703: 704: 705: 706: 707: 708: 709:
710: protected function _isPostalAddress($attribute)
711: {
712: 713:
714: return ($this->_getSyntax($attribute) == '1.3.6.1.4.1.1466.115.121.1.41');
715: }
716:
717: 718: 719: 720: 721: 722: 723: 724:
725: protected function _getSyntax($att)
726: {
727: $ldap = new Horde_Ldap($this->_convertParameters($this->_params));
728: $schema = $ldap->schema();
729:
730: if (!isset($this->_syntaxCache[$att])) {
731: $attv = $schema->get('attribute', $att);
732: $this->_syntaxCache[$att] = isset($attv['syntax'])
733: ? $attv['syntax']
734: : $this->_getSyntax($attv['sup'][0]);
735: }
736:
737: return $this->_syntaxCache[$att];
738: }
739:
740: 741: 742: 743: 744: 745: 746:
747: protected function _convertParameters(array $in)
748: {
749: $map = array(
750: 'server' => 'hostspec',
751: 'port' => 'port',
752: 'tls' => 'tls',
753: 'version' => 'version',
754: 'root' => 'basedn',
755: 'bind_dn' => 'binddn',
756: 'bind_password' => 'bindpw',
757:
758:
759:
760: 'scope' => 'scope',
761:
762:
763:
764:
765:
766:
767:
768: );
769: $out = array();
770: foreach ($in as $key => $value) {
771: if (isset($map[$key])) {
772: $out[$map[$key]] = $value;
773: }
774: }
775: return $out;
776: }
777: }
778: