1: <?php
2:
3: App::uses('Basics', 'Base.Lib');
4: App::uses('LdapUtils', 'Ldap.Lib');
5: App::uses('LdapObjectNotWritableException', 'Ldap.Lib');
6:
7: class Ldap extends DataSource {
8:
9: const LDAP_ERROR_NO_SUCH_OBJECT = 32;
10:
11: public $connection = null;
12:
13: protected $_baseConfig = array(
14: 'host' => 'localhost',
15: 'version' => 3,
16: 'ssl' => false,
17: );
18:
19: private $_modelBaseConfig = array(
20: 'relativeBaseDn' => '',
21: );
22:
23: public function __construct($config = null) {
24: parent::__construct($config);
25: $this->connection = $this->_buildConnection();
26: if (!@ldap_bind($this->connection, $this->config['login'], $this->config['password'])) {
27: $this->_throwPhysicalConnectionException("Datasource not connected");
28: }
29: }
30:
31: private function _buildConnection() {
32: $url = ($this->config['ssl'] ? 'ldaps' : 'ldap') . '://' . $this->config['host'];
33:
34: if (!empty($this->config['port'])) {
35: $url .= ':' . $this->config['port'];
36: }
37:
38: $connection = ldap_connect($url);
39:
40: if (!$connection) {
41: throw new Exception("Not connected");
42: }
43:
44: ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
45:
46: return $connection;
47: }
48:
49: public function column($real) {
50: return $real;
51: }
52:
53: public function create(\Model $model, $fields = null, $values = null) {
54: if ($fields == null) {
55: unset($fields, $values);
56: $fields = array_keys($model->data);
57: $values = array_values($model->data);
58: }
59:
60: $modelData = array();
61: for ($i = 0; $i < count($fields); $i++) {
62: $modelData[$fields[$i]] = $values[$i];
63: }
64:
65: $ldapData = array(
66: 'objectClass' => $this->_getModelConfig($model, 'objectClass')
67: ) + $this->_toLdapData($model, $modelData);
68: $dn = $this->buildDnByData($model, $modelData);
69: $this->_throwExceptionIfIsNotWritable($model, $dn);
70:
71: unset($ldapData['dn']);
72:
73: if (@ldap_add($this->connection, $dn, $ldapData)) {
74: $model->id = $dn;
75: return true;
76: } else {
77:
78:
79: $this->_throwPhysicalConnectionException(print_r(compact('dn', 'ldapData'), true));
80: }
81: }
82:
83: 84: 85: 86: 87: 88: 89: 90: 91:
92: function read(\Model $model, $queryData = array(), $recursive = null) {
93: $queryData = $this->__scrubQueryData($queryData);
94: $search = $this->_searchParameters($model, $queryData);
95:
96: $searchResult = @ldap_search(
97: $this->connection
98: , $search['baseDn']
99: , $search['filter']
100: , $search['attributes']
101: , $search['attributesOnly']
102: , $search['sizeLimit']
103: , $search['timeLimit']
104: , $search['deref']
105: );
106:
107: if ($searchResult === false) {
108: if (ldap_errno($this->connection) == self::LDAP_ERROR_NO_SUCH_OBJECT) {
109: return array();
110: }
111:
112: $this->_throwPhysicalConnectionException(print_r($search,true));
113: $model->onError();
114: return false;
115: }
116:
117: $info = ldap_get_entries($this->connection, $searchResult);
118: if ($search['excludeBase']) {
119: $infoBefore = $info;
120: $info = $this->_excludeDn($info, $search['baseDn']);
121: }
122:
123: if ($this->_isQueryCount($queryData)) {
124: $result[0][$model->alias]['count'] = $info['count'];
125: return $result;
126: }
127:
128: unset($info['count']);
129:
130: $modelInstances = array();
131:
132: foreach($info as $ldapInstance) {
133: $modelInstances[][$model->alias] = $this->_fromLdapData(
134: $model
135: , $ldapInstance);
136: }
137:
138: return $modelInstances;
139: }
140:
141: private function _searchParameters(Model $model, $queryData) {
142: $conditions = $this->_parseConditions($model, $queryData['conditions']);
143:
144: if (array_key_exists("{$model->alias}.{$model->primaryKey}", $conditions)) {
145: $baseDn = $queryData['conditions']["{$model->alias}.{$model->primaryKey}"];
146: $filter = '(objectclass=*)';
147: $excludeBase = false;
148: } else {
149: $baseDn = $this->_getModelBaseDn($model);
150: $filter = $this->_conditions($model, $conditions);
151: $excludeBase = true;
152: }
153:
154: $attributes = array();
155: $attributesOnly = null;
156: $sizeLimit = null;
157: $timeLimit = null;
158: $deref = null;
159:
160: return compact(
161: 'baseDn'
162: , 'filter'
163: , 'attributes'
164: , 'attributesOnly'
165: , 'sizeLimit'
166: , 'timeLimit'
167: , 'deref'
168: , 'excludeBase'
169: );
170: }
171:
172: private function _parseConditions($model, $conditions) {
173: if (is_string($conditions)) {
174: if (preg_match('/^([^=])+=(.+)$/', $conditions, $matches)) {
175: return $this->_parseConditions($model, array(
176: $matches[1] => $matches[2]
177: ));
178: }
179: else {
180: throw new Exception("Condition pattern not recognized: $conditions");
181: }
182: }
183: if (is_array ($conditions)) {
184: $parsedConditions = array();
185: foreach($conditions as $key => $value) {
186: if (!is_string($value)) {
187: throw new Exception("Condition value is not a string");
188: }
189:
190: $parsedConditions[Basics::fieldFullName($key, $model->alias)] = $value;
191: }
192: return $parsedConditions;
193: }
194: else {
195: throw new Exception('$conditions is not string neither array');
196: }
197: }
198:
199: private function _isQueryCount($queryData) {
200: return is_string($queryData['fields']) &&
201: $queryData['fields'] == 'COUNT(*) AS ' . $this->column('count');
202: }
203:
204: public function update(\Model $model, $fields = null, $values = null, $conditions = null) {
205: if ($conditions !== null) {
206: throw new NotImplementedException("Unsuported update() call with \"conditions\" parameter");
207: }
208:
209: if ($fields == null) {
210: unset($fields, $values);
211: $fields = array_keys($model->data);
212: $values = array_values($model->data);
213: }
214:
215: $modelData = array();
216: for ($i = 0; $i < count($fields); $i++) {
217: $modelData[$fields[$i]] = $values[$i];
218: }
219:
220: if (empty($modelData[$model->primaryKey])) {
221: $modelData[$model->primaryKey] = $model->id;
222: }
223:
224: $ldapData = $this->_toLdapData($model, $modelData);
225:
226: if (!empty($modelData[$model->primaryKey])) {
227: $dn = $modelData[$model->primaryKey];
228: }
229: else if (!empty($model->id)) {
230: $dn = $model->id;
231: }
232: else {
233: throw new Exception("No primary key value was defined");
234: }
235:
236: $this->_throwExceptionIfIsNotWritable($model, $dn);
237: unset($ldapData['dn']);
238:
239: $rdnAttribute = $this->_rdnAttribute($dn);
240: if (isset($ldapData[$rdnAttribute])) {
241: if ($ldapData[$rdnAttribute] != LdapUtils::firstRdn($dn, 'value')) {
242: $dn = $this->_renameRdn($dn, $ldapData[$rdnAttribute]);
243: }
244: unset($ldapData[$rdnAttribute]);
245: if (empty($ldapData)) {
246: $model->id = $dn;
247: return true;
248: }
249: }
250:
251: if (@ldap_modify($this->connection, $dn, $ldapData)) {
252: $model->id = $dn;
253: return true;
254: } else {
255:
256:
257: $this->_throwPhysicalConnectionException(print_r(compact('dn', 'ldapData'), true));
258: }
259: }
260:
261: public function calculate(&$model, $func, $params = array()) {
262: $params = (array)$params;
263:
264: switch (strtolower($func)) {
265: case 'count':
266: if (!isset($params[0])) {
267: $params[0] = '*';
268: }
269: if (!isset($params[1])) {
270: $params[1] = 'count';
271: }
272: return 'COUNT(' . $this->column($params[0]) . ') AS ' . $this->column($params[1]);
273: case 'max':
274: case 'min':
275: if (!isset($params[1])) {
276: $params[1] = $params[0];
277: }
278: return strtoupper($func) . '(' . $this->column($params[0]) . ') AS ' . $this->column($params[1]);
279: break;
280: }
281: }
282:
283: public function delete(\Model $model, $id = null) {
284: if (!$id) {
285: $id = array(
286: "{$model->alias}.{$model->primaryKey}"=> $this->id
287: );
288: }
289:
290: $instances = $model->find(
291: 'all', array(
292: 'conditions' => $id
293: )
294: );
295:
296: if (empty($instances)) {
297: return false;
298: }
299:
300: foreach($instances as $instance) {
301: if (!@ldap_delete($this->connection, $instance[$model->alias][$model->primaryKey])) {
302: return false;
303: }
304: }
305:
306: return true;
307: }
308:
309: public function query() {
310: $args = func_get_args();
311: $fields = null;
312: $order = null;
313: $limit = null;
314: $page = null;
315: $recursive = null;
316:
317: if (count($args) === 1) {
318: throw new Exception('count($args) === 1');
319: } elseif (count($args) > 1 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) {
320: $params = $args[1];
321:
322: if (substr($args[0], 0, 6) === 'findBy') {
323: $all = false;
324: $field = Inflector::underscore(substr($args[0], 6));
325: } else {
326: $all = true;
327: $field = Inflector::underscore(substr($args[0], 9));
328: }
329:
330: $or = (strpos($field, '_or_') !== false);
331: if ($or) {
332: $field = explode('_or_', $field);
333: } else {
334: $field = explode('_and_', $field);
335: }
336: $off = count($field) - 1;
337:
338: if (isset($params[1 + $off])) {
339: $fields = $params[1 + $off];
340: }
341:
342: if (isset($params[2 + $off])) {
343: $order = $params[2 + $off];
344: }
345:
346: if (!array_key_exists(0, $params)) {
347: return false;
348: }
349:
350: $c = 0;
351: $conditions = array();
352:
353: foreach ($field as $f) {
354: $conditions[$args[2]->alias . '.' . $f] = $params[$c++];
355: }
356:
357: if ($or) {
358: $conditions = array('OR' => $conditions);
359: }
360:
361: if ($all) {
362: if (isset($params[3 + $off])) {
363: $limit = $params[3 + $off];
364: }
365:
366: if (isset($params[4 + $off])) {
367: $page = $params[4 + $off];
368: }
369:
370: if (isset($params[5 + $off])) {
371: $recursive = $params[5 + $off];
372: }
373: return $args[2]->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
374: } else {
375: if (isset($params[3 + $off])) {
376: $recursive = $params[3 + $off];
377: }
378: return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive'));
379: }
380: } else {
381: throw new Exception("Method not found: {$args[0]}");
382: }
383: }
384:
385: public function bind($dn, $password) {
386: return @ldap_bind(
387: $this->_buildConnection()
388: , $dn
389: , $password
390: );
391: }
392:
393: public function describe($model) {
394: if (empty($model->schema)) {
395: throw new Exception("{$model->name} has no attribute '\$schema' defined");
396: }
397:
398: $schema = array($model->primaryKey => array('type' => 'string')) + $model->schema;
399:
400: foreach(array_keys($schema) as $field) {
401: $schema[$field] += array(
402: 'type' => 'string',
403: 'length' => null,
404: 'null' => false
405: );
406: }
407:
408: return $schema;
409: }
410:
411: 412: 413: 414: 415: 416: 417:
418: public function _conditions(Model $model, $modelConditions) {
419: $modelData = array();
420:
421: foreach($modelConditions as $modelField => $value) {
422: list($alias,$field) = Basics::fieldNameToArray($modelField);
423: if ($alias != $model->alias) {
424: throw new NotImplementedException("Conditions with alias then self model: {$modelField} in {$model->alias}");
425: }
426: $modelData[$field] = $value;
427: }
428:
429: $ldapData = array();
430: foreach($this->_toLdapData($model, $modelData) as $attribute => $value) {
431: $ldapData[] = array($attribute, $this->_quote($value));
432: }
433:
434: $ldapData[] = array('objectClass', $this->_getModelConfig($model, 'objectClass'));
435: return $this->_conditionsArrayToString($ldapData);
436: }
437: 438: 439: 440: 441: 442:
443: function _conditionsArrayToString($conditions, $join = '&') {
444: if (empty($conditions)) {
445: return null;
446: }
447: else {
448: reset($conditions);
449: list($attribute, $value) = $conditions[key($conditions)];
450: unset($conditions[key($conditions)]);
451:
452: $currentCondition = $this->_conditionAttributeValue($attribute, $value);
453: $leftConditions = $this->_conditionsArrayToString($conditions);
454: return $leftConditions ? '(' . $join . $currentCondition . $leftConditions . ')' : "$currentCondition";
455: }
456: }
457:
458: private function _conditionAttributeValue($attribute, $value) {
459: if (is_array($value)) {
460: $conditions = array();
461: foreach ($value as $subValue) {
462: $conditions = array(array($attribute, $subValue));
463: }
464: return $this->_conditionsArrayToString($conditions, '|');
465: } else {
466: return "($attribute=$value)";
467: }
468: }
469:
470: private function _quote($str) {
471: return str_replace(
472: array('\\', ' ', '*', '(', ')')
473: , array('\\5c', '\\20', '\\2a', '\\28', '\\29'), $str
474: );
475: }
476:
477: 478: 479: 480: 481:
482: function __scrubQueryData($queryData) {
483: if (!isset ($queryData['type']))
484: $queryData['type'] = 'default';
485:
486: if (!isset ($queryData['conditions']))
487: $queryData['conditions'] = array();
488:
489: if (!isset ($queryData['fields']) && empty($queryData['fields']))
490: $queryData['fields'] = array ();
491:
492: if (!isset ($queryData['order']) && empty($queryData['order']))
493: $queryData['order'] = array ();
494:
495: if (!isset ($queryData['limit']))
496: $queryData['limit'] = null;
497:
498: return $queryData;
499: }
500:
501: private function _toLdapData(Model $model, $modelData) {
502: $method = $this->_getDatabaseMethod($model, 'ToLdap');
503: if ($method->getNumberOfParameters() > 1) {
504: $ldapData = $method->invoke(
505: ConnectionManager::$config
506: , $modelData
507: , $this->_previousLdapData($model, $modelData)
508: );
509: } else {
510: $ldapData = $method->invoke(
511: ConnectionManager::$config
512: , $modelData);
513: }
514:
515: unset($ldapData['dn']);
516: if (!empty($modelData[$model->primaryKey])) {
517: $ldapData['dn'] = $modelData[$model->primaryKey];
518: }
519:
520: return $ldapData;
521: }
522:
523: private function _fromLdapData(Model $model, $ldapData) {
524: unset($ldapData['objectclass']);
525: unset($ldapData['count']);
526:
527: foreach ($ldapData as $key => $value) {
528: if (is_numeric($key)) {
529: unset($ldapData[$key]);
530: } else if (is_array($value)) {
531: $ldapData[$key] = array_key_exists(0, $value) ?
532: $value[0] :
533: null;
534: }
535: }
536:
537: $modelData = $this->_getDatabaseMethod($model, 'FromLdap')->invoke(
538: ConnectionManager::$config
539: , $ldapData);
540:
541: if (!empty($ldapData['dn'])) {
542: $modelData[$model->primaryKey] = LdapUtils::normalizeDn($ldapData['dn']);
543: }
544:
545:
546: return $modelData;
547: }
548:
549: 550: 551: 552: 553: 554: 555:
556: private function _getDatabaseMethod($model, $suffix) {
557: $class = new ReflectionClass($model);
558:
559: $methods = array();
560:
561: while ($class) {
562: $databaseToLdapMethod = '__' . $model->useDbConfig . $class->getName() . $suffix;
563:
564: if (method_exists(ConnectionManager::$config, $databaseToLdapMethod)) {
565: return new ReflectionMethod(ConnectionManager::$config, $databaseToLdapMethod);
566: }
567:
568: $class = $class->getParentClass();
569: $methods[] = $databaseToLdapMethod;
570: }
571:
572: throw new Exception("Class \"" . get_class(ConnectionManager::$config) . "\" has no method " . print_r($methods, true));
573: }
574:
575: public function buildDnByData(Model $model, $modelData) {
576: $ldapData = $this->_toLdapData($model, $modelData);
577: $dnAttribute = $this->_getModelConfig($model, 'dnAttribute');
578:
579: if (empty($ldapData[$dnAttribute])) {
580: throw new Exception("Ldap data has no DN attribute \"$dnAttribute\"");
581: }
582:
583: $modelDn = $this->_getModelWritableBaseDn($model);
584: return LdapUtils::normalizeDn("$dnAttribute={$ldapData[$dnAttribute]}" . ($modelDn ? ',' . $modelDn : ''));
585: }
586:
587: private function _getModelBaseDn(Model $model) {
588: $modelDn = $this->_getModelConfig($model, 'relativeBaseDn');
589: $dataSourceDn = $this->config['database'] ? $this->config['database'] : '';
590:
591: if ($modelDn && $dataSourceDn) {
592: return LdapUtils::normalizeDn($modelDn . ',' . $dataSourceDn);
593: } else {
594: return LdapUtils::normalizeDn($modelDn . $dataSourceDn);
595: }
596: }
597:
598: private function _getModelWritableBaseDn(\Model $model) {
599: return LdapUtils::joinDns(
600: $this->_getModelConfig($model, 'writableRelativeBaseDn', false, '')
601: , $this->_getModelBaseDn($model)
602: );
603: }
604:
605: private function _getModelConfig(Model $model, $key, $required = true, $defaultValue = null) {
606: $class = new ReflectionClass($model);
607:
608: while ($class) {
609: if (isset($this->config['models'][$class->getName()][$key])) {
610: return $this->config['models'][$class->getName()][$key];
611: }
612:
613: $class = $class->getParentClass();
614: }
615:
616: if (!empty($this->_modelBaseConfig[$key])) {
617: return $this->_modelBaseConfig[$key];
618: }
619: if ($required) {
620: throw new Exception("No config '$key' defined for model \"{$model->name}\"");
621: }
622: else {
623: return $defaultValue;
624: }
625: }
626:
627: private function _throwPhysicalConnectionException($message) {
628: $errorCode = ldap_errno($this->connection);
629: throw new Exception(
630: ldap_err2str($errorCode) . " (Code: $errorCode)" .
631: ($message ? "\n$message" : '')
632: );
633: }
634:
635: private function _rdnAttribute($dn) {
636: if (($firstEqualsPosition = strpos($dn, '=')) === false) {
637: throw new Exception("DN bad formatted: $dn");
638: } else {
639: return substr($dn, 0, $firstEqualsPosition);
640: }
641: }
642:
643: 644: 645: 646: 647: 648:
649: private function _renameRdn($dn, $rdnValue) {
650: $rdn = $this->_rdnAttribute($dn) . '=' . $rdnValue;
651: $parentDn = $this->_parentDn($dn);
652:
653:
654: if (ldap_rename(
655: $this->connection
656: , $dn
657: , $rdn
658: , $parentDn
659: , true
660: )) {
661: return $this->_getRenamedDn($dn, $rdn);
662: } else {
663: $this->_throwPhysicalConnectionException(print_r(compact('dn','rdnValue','rdn'),true));
664: }
665: }
666:
667: private function _getRenamedDn($dn,$rdn) {
668: return $rdn . ',' . $this->_parentDn($dn);
669: }
670:
671: private function _parentDn($dn) {
672: $parts = ldap_explode_dn($dn, 0);
673: unset($parts['count']);
674: array_shift($parts);
675: return implode(',', $parts);
676: }
677:
678: private function _excludeDn($entries, $dn) {
679: $dn = LdapUtils::normalizeDn($dn);
680: $newEntries = array();
681: for ($i = 0; $i < $entries['count']; $i++) {
682: if ($this->_entryDn($entries[$i]) != $dn) {
683: $newEntries[] = $entries[$i];
684: }
685: }
686:
687: $newEntries['count'] = count($newEntries);
688: return $newEntries;
689: }
690:
691: private function _entryDn($entry) {
692: foreach (array('dn', 'DN', 'Dn', 'dN') as $key) {
693: if (isset($entry[$key])) {
694: return LdapUtils::normalizeDn($entry[$key]);
695: }
696: }
697:
698: throw new Exception("Entry has no DN attribute");
699: }
700:
701: private function _previousLdapData($model, $modelData) {
702: if (!empty($modelData[$model->primaryKey])) {
703: $id = $modelData[$model->primaryKey];
704: } 705: 706:
707:
708: if (!empty($id)) {
709: $previousData = $model->find(
710: 'first', array(
711: 'conditions' => array(
712: "{$model->alias}.{$model->primaryKey}" => $id
713: )
714: ));
715:
716: $previousData = empty($previousData) ?
717: false :
718: $previousData[$model->alias];
719: } else {
720: $previousData = false;
721: }
722:
723: return $previousData;
724: }
725:
726: private function _throwExceptionIfIsNotWritable(\Model $model, $dn) {
727: if (!$this->isWritable($model, $dn)) {
728: throw new LdapObjectNotWritableException(
729: $model
730: , $dn
731: , $this->_getModelWritableBaseDn($model));
732: }
733: }
734:
735: public function isWritable(\Model $model, $dn) {
736: return LdapUtils::isDnParent(
737: $this->_getModelWritableBaseDn($model)
738: , $dn
739: );
740: }
741:
742: }
743: ?>