1: <?php
2: /**
3: * Load an LDAP Schema and provide information
4: *
5: * This class takes a Subschema entry, parses this information
6: * and makes it available in an array. Most of the code has been
7: * inspired by perl-ldap( http://perl-ldap.sourceforge.net).
8: * You will find portions of their implementation in here.
9: *
10: * Copyright 2009 Jan Wagner, Benedikt Hallinger
11: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
12: *
13: * @category Horde
14: * @package Ldap
15: * @author Jan Wagner <wagner@netsols.de>
16: * @author Benedikt Hallinger <beni@php.net>
17: * @author Jan Schneider <jan@horde.org>
18: * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
19: */
20: class Horde_Ldap_Schema
21: {
22: /**
23: * Syntax definitions.
24: *
25: * Please don't forget to add binary attributes to isBinary() below to
26: * support proper value fetching from Horde_Ldap_Entry.
27: */
28: const SYNTAX_BOOLEAN = '1.3.6.1.4.1.1466.115.121.1.7';
29: const SYNTAX_DIRECTORY_STRING = '1.3.6.1.4.1.1466.115.121.1.15';
30: const SYNTAX_DISTINGUISHED_NAME = '1.3.6.1.4.1.1466.115.121.1.12';
31: const SYNTAX_INTEGER = '1.3.6.1.4.1.1466.115.121.1.27';
32: const SYNTAX_JPEG = '1.3.6.1.4.1.1466.115.121.1.28';
33: const SYNTAX_NUMERIC_STRING = '1.3.6.1.4.1.1466.115.121.1.36';
34: const SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.38';
35: const SYNTAX_OCTET_STRING = '1.3.6.1.4.1.1466.115.121.1.40';
36:
37: /**
38: * Map of entry types to LDAP attributes of subschema entry.
39: *
40: * @var array
41: */
42: public $types = array(
43: 'attribute' => 'attributeTypes',
44: 'ditcontentrule' => 'dITContentRules',
45: 'ditstructurerule' => 'dITStructureRules',
46: 'matchingrule' => 'matchingRules',
47: 'matchingruleuse' => 'matchingRuleUse',
48: 'nameform' => 'nameForms',
49: 'objectclass' => 'objectClasses',
50: 'syntax' => 'ldapSyntaxes' );
51:
52: /**
53: * Array of entries belonging to this type
54: *
55: * @var array
56: */
57: protected $_attributeTypes = array();
58: protected $_matchingRules = array();
59: protected $_matchingRuleUse = array();
60: protected $_ldapSyntaxes = array();
61: protected $_objectClasses = array();
62: protected $_dITContentRules = array();
63: protected $_dITStructureRules = array();
64: protected $_nameForms = array();
65:
66:
67: /**
68: * Hash of all fetched OIDs.
69: *
70: * @var array
71: */
72: protected $_oids = array();
73:
74: /**
75: * Whether the schema is initialized.
76: *
77: * @see parse(), get()
78: * @var boolean
79: */
80: protected $_initialized = false;
81:
82: /**
83: * Constructor.
84: *
85: * Fetches the Schema from an LDAP connection.
86: *
87: * @param Horde_Ldap $ldap LDAP connection.
88: * @param string $dn Subschema entry DN.
89: *
90: * @throws Horde_Ldap_Exception
91: */
92: public function __construct(Horde_Ldap $ldap, $dn = null)
93: {
94: if (is_null($dn)) {
95: // Get the subschema entry via rootDSE.
96: $dse = $ldap->rootDSE(array('subschemaSubentry'));
97: $base = $dse->getValue('subschemaSubentry', 'single');
98: $dn = $base;
99: }
100:
101: // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that
102: // incorrectly call this entry subSchemaSubentry instead of
103: // subschemaSubentry. Note the correct case/spelling as per RFC 2251.
104: if (is_null($dn)) {
105: // Get the subschema entry via rootDSE.
106: $dse = $ldap->rootDSE(array('subSchemaSubentry'));
107: $base = $dse->getValue('subSchemaSubentry', 'single');
108: $dn = $base;
109: }
110:
111: // Final fallback in case there is no subschemaSubentry attribute in
112: // the root DSE (this is a bug for an LDAPv3 server so report this to
113: // your LDAP vendor if you get this far).
114: if (is_null($dn)) {
115: $dn = 'cn=Subschema';
116: }
117:
118: // Fetch the subschema entry.
119: $result = $ldap->search($dn, '(objectClass=*)',
120: array('attributes' => array_values($this->types),
121: 'scope' => 'base'));
122: $entry = $result->shiftEntry();
123: if (!($entry instanceof Horde_Ldap_Entry)) {
124: throw new Horde_Ldap_Exception('Could not fetch Subschema entry');
125: }
126:
127: $this->parse($entry);
128: }
129:
130: /**
131: * Returns a hash of entries for the given type.
132: *
133: * Types may be: objectclasses, attributes, ditcontentrules,
134: * ditstructurerules, matchingrules, matchingruleuses, nameforms, syntaxes.
135: *
136: * @param string $type Type to fetch.
137: *
138: * @return array
139: * @throws Horde_Ldap_Exception
140: */
141: public function getAll($type)
142: {
143: $map = array('objectclasses' => $this->_objectClasses,
144: 'attributes' => $this->_attributeTypes,
145: 'ditcontentrules' => $this->_dITContentRules,
146: 'ditstructurerules' => $this->_dITStructureRules,
147: 'matchingrules' => $this->_matchingRules,
148: 'matchingruleuses' => $this->_matchingRuleUse,
149: 'nameforms' => $this->_nameForms,
150: 'syntaxes' => $this->_ldapSyntaxes);
151:
152: $key = Horde_String::lower($type);
153: if (!isset($map[$key])) {
154: throw new Horde_Ldap_Exception("Unknown type $type");
155: }
156:
157: return $map[$key];
158: }
159:
160: /**
161: * Returns a specific entry.
162: *
163: * @param string $type Type of name.
164: * @param string $name Name or OID to fetch.
165: *
166: * @return mixed
167: * @throws Horde_Ldap_Exception
168: */
169: public function get($type, $name)
170: {
171: if (!$this->_initialized) {
172: return null;
173: }
174:
175: $type = Horde_String::lower($type);
176: if (!isset($this->types[$type])) {
177: throw new Horde_Ldap_Exception("No such type $type");
178: }
179:
180: $name = Horde_String::lower($name);
181: $type_var = $this->{'_' . $this->types[$type]};
182:
183: if (isset($type_var[$name])) {
184: return $type_var[$name];
185: }
186: if (isset($this->_oids[$name]) &&
187: $this->_oids[$name]['type'] == $type) {
188: return $this->_oids[$name];
189: }
190: throw new Horde_Ldap_Exception("Could not find $type $name");
191: }
192:
193:
194: /**
195: * Fetches attributes that MAY be present in the given objectclass.
196: *
197: * @param string $oc Name or OID of objectclass.
198: * @param boolean $checksup Check all superiour objectclasses too?
199: *
200: * @return array Array with attributes.
201: */
202: public function may($oc, $checksup = false)
203: {
204: try {
205: $attributes = $this->_getAttr($oc, 'may');
206: } catch (Horde_Ldap_Exception $e) {
207: $attributes = array();
208: }
209: if ($checksup) {
210: try {
211: foreach ($this->superclass($oc) as $sup) {
212: $attributes = array_merge($attributes, $this->may($sup, true));
213: }
214: } catch (Horde_Ldap_Exception $e) {
215: }
216: $attributes = array_values(array_unique($attributes));
217: }
218: return $attributes;
219: }
220:
221: /**
222: * Fetches attributes that MUST be present in the given objectclass.
223: *
224: * @param string $oc Name or OID of objectclass.
225: * @param boolean $checksup Check all superiour objectclasses too?
226: *
227: * @return array Array with attributes.
228: */
229: public function must($oc, $checksup = false)
230: {
231: try {
232: $attributes = $this->_getAttr($oc, 'must');
233: } catch (Horde_Ldap_Exception $e) {
234: $attributes = array();
235: }
236: if ($checksup) {
237: try {
238: foreach ($this->superclass($oc) as $sup) {
239: $attributes = array_merge($attributes, $this->must($sup, true));
240: }
241: } catch (Horde_Ldap_Exception $e) {
242: }
243: $attributes = array_values(array_unique($attributes));
244: }
245: return $attributes;
246: }
247:
248: /**
249: * Fetches the given attribute from the given objectclass.
250: *
251: * @param string $oc Name or OID of objectclass.
252: * @param string $attr Name of attribute to fetch.
253: *
254: * @return array The attribute.
255: * @throws Horde_Ldap_Exception
256: */
257: protected function _getAttr($oc, $attr)
258: {
259: $oc = Horde_String::lower($oc);
260: if (isset($this->_objectClasses[$oc]) &&
261: isset($this->_objectClasses[$oc][$attr])) {
262: return $this->_objectClasses[$oc][$attr];
263: }
264: if (isset($this->_oids[$oc]) &&
265: $this->_oids[$oc]['type'] == 'objectclass' &&
266: isset($this->_oids[$oc][$attr])) {
267: return $this->_oids[$oc][$attr];
268: }
269: throw new Horde_Ldap_Exception("Could not find $attr attributes for $oc ");
270: }
271:
272: /**
273: * Returns the name(s) of the immediate superclass(es).
274: *
275: * @param string $oc Name or OID of objectclass.
276: *
277: * @return array
278: * @throws Horde_Ldap_Exception
279: */
280: public function superclass($oc)
281: {
282: $o = $this->get('objectclass', $oc);
283: return isset($o['sup']) ? $o['sup'] : array();
284: }
285:
286: /**
287: * Parses the schema of the given subschema entry.
288: *
289: * @param Horde_Ldap_Entry $entry Subschema entry.
290: */
291: public function parse($entry)
292: {
293: foreach ($this->types as $type => $attr) {
294: // Initialize map type to entry.
295: $type_var = '_' . $attr;
296: $this->{$type_var} = array();
297:
298: if (!$entry->exists($attr)) {
299: continue;
300: }
301:
302: // Get values for this type.
303: $values = $entry->getValue($attr);
304: if (!is_array($values)) {
305: continue;
306: }
307: foreach ($values as $value) {
308: // Get the schema entry.
309: $schema_entry = $this->_parse_entry($value);
310: // Set the type.
311: $schema_entry['type'] = $type;
312: // Save a ref in $_oids.
313: $this->_oids[$schema_entry['oid']] = $schema_entry;
314: // Save refs for all names in type map.
315: $names = $schema_entry['aliases'];
316: array_push($names, $schema_entry['name']);
317: foreach ($names as $name) {
318: $this->{$type_var}[Horde_String::lower($name)] = $schema_entry;
319: }
320: }
321: }
322: $this->_initialized = true;
323: }
324:
325: /**
326: * Parses an attribute value into a schema entry.
327: *
328: * @param string $value Attribute value.
329: *
330: * @return array Schema entry array.
331: */
332: protected function _parse_entry($value)
333: {
334: // Tokens that have no value associated.
335: $noValue = array('single-value',
336: 'obsolete',
337: 'collective',
338: 'no-user-modification',
339: 'abstract',
340: 'structural',
341: 'auxiliary');
342:
343: // Tokens that can have multiple values.
344: $multiValue = array('must', 'may', 'sup');
345:
346: // Get an array of tokens.
347: $tokens = $this->_tokenize($value);
348:
349: // Remove surrounding brackets.
350: if ($tokens[0] == '(') {
351: array_shift($tokens);
352: }
353: if ($tokens[count($tokens) - 1] == ')') {
354: array_pop($tokens);
355: }
356:
357: // First token is the oid.
358: $schema_entry = array('aliases' => array(),
359: 'oid' => array_shift($tokens));
360:
361: // Cycle over the tokens until none are left.
362: while (count($tokens) > 0) {
363: $token = Horde_String::lower(array_shift($tokens));
364: if (in_array($token, $noValue)) {
365: // Single value token.
366: $schema_entry[$token] = 1;
367: } else {
368: // Follow a string or a list if it is multivalued.
369: if (($schema_entry[$token] = array_shift($tokens)) == '(') {
370: // Create the list of values and cycles through the tokens
371: // until the end of the list is reached ')'.
372: $schema_entry[$token] = array();
373: while ($tmp = array_shift($tokens)) {
374: if ($tmp == ')') {
375: break;
376: }
377: if ($tmp != '$') {
378: array_push($schema_entry[$token], $tmp);
379: }
380: }
381: }
382: // Create an array if the value should be multivalued but was
383: // not.
384: if (in_array($token, $multiValue) &&
385: !is_array($schema_entry[$token])) {
386: $schema_entry[$token] = array($schema_entry[$token]);
387: }
388: }
389: }
390:
391: // Get max length from syntax.
392: if (isset($schema_entry['syntax'])) {
393: if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
394: $schema_entry['max_length'] = $matches[1];
395: }
396: }
397:
398: // Force a name.
399: if (empty($schema_entry['name'])) {
400: $schema_entry['name'] = $schema_entry['oid'];
401: }
402:
403: // Make one name the default and put the other ones into aliases.
404: if (is_array($schema_entry['name'])) {
405: $aliases = $schema_entry['name'];
406: $schema_entry['name'] = array_shift($aliases);
407: $schema_entry['aliases'] = $aliases;
408: }
409:
410: return $schema_entry;
411: }
412:
413: /**
414: * Tokenizes the given value into an array of tokens.
415: *
416: * @param string $value String to parse.
417: *
418: * @return array Array of tokens.
419: */
420: protected function _tokenize($value)
421: {
422: /* Match one big pattern where only one of the three subpatterns
423: * matches. We are interested in the subpatterns that matched. If it
424: * matched its value will be non-empty and so it is a token. Tokens may
425: * be round brackets, a string, or a string enclosed by ''. */
426: preg_match_all("/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x", $value, $matches);
427:
428: $tokens = array();
429: // Number of tokens (full pattern match).
430: for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
431: // Each subpattern.
432: for ($j = 1; $j < 4; $j++) {
433: // Pattern match in this subpattern.
434: if (null != trim($matches[$j][$i])) {
435: // This is the token.
436: $tokens[$i] = trim($matches[$j][$i]);
437: }
438: }
439: }
440:
441: return $tokens;
442: }
443:
444: /**
445: * Returns wether a attribute syntax is binary or not.
446: *
447: * This method is used by Horde_Ldap_Entry to decide which PHP function
448: * needs to be used to fetch the value in the proper format (e.g. binary or
449: * string).
450: *
451: * @param string $attribute The name of the attribute (eg.: 'sn').
452: *
453: * @return boolean True if the attribute is a binary type.
454: */
455: public function isBinary($attribute)
456: {
457: // All syntax that should be treaten as containing binary values.
458: $syntax_binary = array(self::SYNTAX_OCTET_STRING, self::SYNTAX_JPEG);
459:
460: // Check Syntax.
461: try {
462: $attr_s = $this->get('attribute', $attribute);
463: } catch (Horde_Ldap_Exception $e) {
464: // Attribute not found in schema, consider attr not binary.
465: return false;
466: }
467:
468: if (isset($attr_s['syntax']) &&
469: in_array($attr_s['syntax'], $syntax_binary)) {
470: // Syntax is defined as binary in schema
471: return true;
472: }
473:
474: // Syntax not defined as binary, or not found if attribute is a
475: // subtype, check superior attribute syntaxes.
476: if (isset($attr_s['sup'])) {
477: foreach ($attr_s['sup'] as $superattr) {
478: if ($this->isBinary($superattr)) {
479: // Stop checking parents since we are binary.
480: return true;
481: }
482: }
483: }
484:
485: return false;
486: }
487: }
488: