2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
4 * File containing the Net_LDAP2_Schema interface class.
10 * @author Jan Wagner <wagner@netsols.de>
11 * @author Benedikt Hallinger <beni@php.net>
12 * @copyright 2009 Jan Wagner, Benedikt Hallinger
13 * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
14 * @version SVN: $Id: Schema.php 286718 2009-08-03 07:30:49Z beni $
15 * @link http://pear.php.net/package/Net_LDAP2/
16 * @todo see the comment at the end of the file
22 require_once 'PEAR.php';
27 * Please don't forget to add binary attributes to isBinary() below
28 * to support proper value fetching from Net_LDAP2_Entry
30 define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7');
31 define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15');
32 define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12');
33 define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27');
34 define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28');
35 define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36');
36 define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38');
37 define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40');
40 * Load an LDAP Schema and provide information
42 * This class takes a Subschema entry, parses this information
43 * and makes it available in an array. Most of the code has been
44 * inspired by perl-ldap( http://perl-ldap.sourceforge.net).
45 * You will find portions of their implementation in here.
49 * @author Jan Wagner <wagner@netsols.de>
50 * @author Benedikt Hallinger <beni@php.net>
51 * @license http://www.gnu.org/copyleft/lesser.html LGPL
52 * @link http://pear.php.net/package/Net_LDAP22/
54 class Net_LDAP2_Schema extends PEAR
57 * Map of entry types to ldap attributes of subschema entry
62 public $types = array(
63 'attribute' => 'attributeTypes',
64 'ditcontentrule' => 'dITContentRules',
65 'ditstructurerule' => 'dITStructureRules',
66 'matchingrule' => 'matchingRules',
67 'matchingruleuse' => 'matchingRuleUse',
68 'nameform' => 'nameForms',
69 'objectclass' => 'objectClasses',
70 'syntax' => 'ldapSyntaxes'
74 * Array of entries belonging to this type
79 protected $_attributeTypes = array();
80 protected $_matchingRules = array();
81 protected $_matchingRuleUse = array();
82 protected $_ldapSyntaxes = array();
83 protected $_objectClasses = array();
84 protected $_dITContentRules = array();
85 protected $_dITStructureRules = array();
86 protected $_nameForms = array();
90 * hash of all fetched oids
95 protected $_oids = array();
98 * Tells if the schema is initialized
102 * @see parse(), get()
104 protected $_initialized = false;
108 * Constructor of the class
112 protected function __construct()
114 $this->PEAR('Net_LDAP2_Error'); // default error class
118 * Fetch the Schema from an LDAP connection
120 * @param Net_LDAP2 $ldap LDAP connection
121 * @param string $dn (optional) Subschema entry dn
124 * @return Net_LDAP2_Schema|NET_LDAP2_Error
126 public function fetch($ldap, $dn = null)
128 if (!$ldap instanceof Net_LDAP2) {
129 return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!");
132 $schema_o = new Net_LDAP2_Schema();
135 // get the subschema entry via root dse
136 $dse = $ldap->rootDSE(array('subschemaSubentry'));
137 if (false == Net_LDAP2::isError($dse)) {
138 $base = $dse->getValue('subschemaSubentry', 'single');
139 if (!Net_LDAP2::isError($base)) {
145 // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly
146 // call this entry subSchemaSubentry instead of subschemaSubentry.
147 // Note the correct case/spelling as per RFC 2251.
149 // get the subschema entry via root dse
150 $dse = $ldap->rootDSE(array('subSchemaSubentry'));
151 if (false == Net_LDAP2::isError($dse)) {
152 $base = $dse->getValue('subSchemaSubentry', 'single');
153 if (!Net_LDAP2::isError($base)) {
159 // Final fallback case where there is no subschemaSubentry attribute
160 // in the root DSE (this is a bug for an LDAP v3 server so report this
161 // to your LDAP vendor if you get this far).
163 $dn = 'cn=Subschema';
166 // fetch the subschema entry
167 $result = $ldap->search($dn, '(objectClass=*)',
168 array('attributes' => array_values($schema_o->types),
170 if (Net_LDAP2::isError($result)) {
174 $entry = $result->shiftEntry();
175 if (!$entry instanceof Net_LDAP2_Entry) {
176 return PEAR::raiseError('Could not fetch Subschema entry');
179 $schema_o->parse($entry);
184 * Return a hash of entries for the given type
186 * Returns a hash of entry for th givene type. Types may be:
187 * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules,
188 * matchingruleuses, nameforms, syntaxes
190 * @param string $type Type to fetch
193 * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error
195 public function &getAll($type)
197 $map = array('objectclasses' => &$this->_objectClasses,
198 'attributes' => &$this->_attributeTypes,
199 'ditcontentrules' => &$this->_dITContentRules,
200 'ditstructurerules' => &$this->_dITStructureRules,
201 'matchingrules' => &$this->_matchingRules,
202 'matchingruleuses' => &$this->_matchingRuleUse,
203 'nameforms' => &$this->_nameForms,
204 'syntaxes' => &$this->_ldapSyntaxes );
206 $key = strtolower($type);
207 $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type"));
212 * Return a specific entry
214 * @param string $type Type of name
215 * @param string $name Name or OID to fetch
218 * @return mixed Entry or Net_LDAP2_Error
220 public function &get($type, $name)
222 if ($this->_initialized) {
223 $type = strtolower($type);
224 if (false == key_exists($type, $this->types)) {
225 return PEAR::raiseError("No such type $type");
228 $name = strtolower($name);
229 $type_var = &$this->{'_' . $this->types[$type]};
231 if (key_exists($name, $type_var)) {
232 return $type_var[$name];
233 } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) {
234 return $this->_oids[$name];
236 return PEAR::raiseError("Could not find $type $name");
246 * Fetches attributes that MAY be present in the given objectclass
248 * @param string $oc Name or OID of objectclass
251 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
253 public function may($oc)
255 return $this->_getAttr($oc, 'may');
259 * Fetches attributes that MUST be present in the given objectclass
261 * @param string $oc Name or OID of objectclass
264 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
266 public function must($oc)
268 return $this->_getAttr($oc, 'must');
272 * Fetches the given attribute from the given objectclass
274 * @param string $oc Name or OID of objectclass
275 * @param string $attr Name of attribute to fetch
278 * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error
280 protected function _getAttr($oc, $attr)
282 $oc = strtolower($oc);
283 if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) {
284 return $this->_objectClasses[$oc][$attr];
285 } elseif (key_exists($oc, $this->_oids) &&
286 $this->_oids[$oc]['type'] == 'objectclass' &&
287 key_exists($attr, $this->_oids[$oc])) {
288 return $this->_oids[$oc][$attr];
290 return PEAR::raiseError("Could not find $attr attributes for $oc ");
295 * Returns the name(s) of the immediate superclass(es)
297 * @param string $oc Name or OID of objectclass
300 * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error
302 public function superclass($oc)
304 $o = $this->get('objectclass', $oc);
305 if (Net_LDAP2::isError($o)) {
308 return (key_exists('sup', $o) ? $o['sup'] : array());
312 * Parses the schema of the given Subschema entry
314 * @param Net_LDAP2_Entry &$entry Subschema entry
319 public function parse(&$entry)
321 foreach ($this->types as $type => $attr) {
322 // initialize map type to entry
323 $type_var = '_' . $attr;
324 $this->{$type_var} = array();
326 // get values for this type
327 if ($entry->exists($attr)) {
328 $values = $entry->getValue($attr);
329 if (is_array($values)) {
330 foreach ($values as $value) {
332 unset($schema_entry); // this was a real mess without it
334 // get the schema entry
335 $schema_entry = $this->_parse_entry($value);
338 $schema_entry['type'] = $type;
340 // save a ref in $_oids
341 $this->_oids[$schema_entry['oid']] = &$schema_entry;
343 // save refs for all names in type map
344 $names = $schema_entry['aliases'];
345 array_push($names, $schema_entry['name']);
346 foreach ($names as $name) {
347 $this->{$type_var}[strtolower($name)] = &$schema_entry;
353 $this->_initialized = true;
357 * Parses an attribute value into a schema entry
359 * @param string $value Attribute value
362 * @return array|false Schema entry array or false
364 protected function &_parse_entry($value)
366 // tokens that have no value associated
367 $noValue = array('single-value',
370 'no-user-modification',
375 // tokens that can have multiple values
376 $multiValue = array('must', 'may', 'sup');
378 $schema_entry = array('aliases' => array()); // initilization
380 $tokens = $this->_tokenize($value); // get an array of tokens
382 // remove surrounding brackets
383 if ($tokens[0] == '(') array_shift($tokens);
384 if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
386 $schema_entry['oid'] = array_shift($tokens); // first token is the oid
388 // cycle over the tokens until none are left
389 while (count($tokens) > 0) {
390 $token = strtolower(array_shift($tokens));
391 if (in_array($token, $noValue)) {
392 $schema_entry[$token] = 1; // single value token
394 // this one follows a string or a list if it is multivalued
395 if (($schema_entry[$token] = array_shift($tokens)) == '(') {
396 // this creates the list of values and cycles through the tokens
397 // until the end of the list is reached ')'
398 $schema_entry[$token] = array();
399 while ($tmp = array_shift($tokens)) {
400 if ($tmp == ')') break;
401 if ($tmp != '$') array_push($schema_entry[$token], $tmp);
404 // create a array if the value should be multivalued but was not
405 if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) {
406 $schema_entry[$token] = array($schema_entry[$token]);
410 // get max length from syntax
411 if (key_exists('syntax', $schema_entry)) {
412 if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
413 $schema_entry['max_length'] = $matches[1];
417 if (empty($schema_entry['name'])) {
418 $schema_entry['name'] = $schema_entry['oid'];
420 // make one name the default and put the other ones into aliases
421 if (is_array($schema_entry['name'])) {
422 $aliases = $schema_entry['name'];
423 $schema_entry['name'] = array_shift($aliases);
424 $schema_entry['aliases'] = $aliases;
426 return $schema_entry;
430 * Tokenizes the given value into an array of tokens
432 * @param string $value String to parse
435 * @return array Array of tokens
437 protected function _tokenize($value)
439 $tokens = array(); // array of tokens
440 $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns
442 // this one is taken from perl-ldap, modified for php
443 $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
446 * This one matches one big pattern wherin only one of the three subpatterns matched
447 * We are interested in the subpatterns that matched. If it matched its value will be
448 * non-empty and so it is a token. Tokens may be round brackets, a string, or a string
451 preg_match_all($pattern, $value, $matches);
453 for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match)
454 for ($j = 1; $j < 4; $j++) { // each subpattern
455 if (null != trim($matches[$j][$i])) { // pattern match in this subpattern
456 $tokens[$i] = trim($matches[$j][$i]); // this is the token
464 * Returns wether a attribute syntax is binary or not
466 * This method gets used by Net_LDAP2_Entry to decide which
467 * PHP function needs to be used to fetch the value in the
468 * proper format (e.g. binary or string)
470 * @param string $attribute The name of the attribute (eg.: 'sn')
475 public function isBinary($attribute)
477 $return = false; // default to false
479 // This list contains all syntax that should be treaten as
480 // containing binary values
481 // The Syntax Definitons go into constants at the top of this page
482 $syntax_binary = array(
483 NET_LDAP2_SYNTAX_OCTET_STRING,
484 NET_LDAP2_SYNTAX_JPEG
488 $attr_s = $this->get('attribute', $attribute);
489 if (Net_LDAP2::isError($attr_s)) {
490 // Attribute not found in schema
491 $return = false; // consider attr not binary
492 } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) {
493 // Syntax is defined as binary in schema
496 // Syntax not defined as binary, or not found
497 // if attribute is a subtype, check superior attribute syntaxes
498 if (isset($attr_s['sup'])) {
499 foreach ($attr_s['sup'] as $superattr) {
500 $return = $this->isBinary($superattr);
502 break; // stop checking parents since we are binary
511 // [TODO] add method that allows us to see to which objectclasses a certain attribute belongs to
512 // it should return the result structured, e.g. sorted in "may" and "must". Optionally it should
513 // be able to return it just "flat", e.g. array_merge()d.
514 // We could use get_all() to achieve this easily, i think