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 296515 2010-03-22 14:46:41Z 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)) {
171 return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage());
174 $entry = $result->shiftEntry();
175 if (!$entry instanceof Net_LDAP2_Entry) {
176 if ($entry instanceof Net_LDAP2_Error) {
177 return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage());
179 return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')');
183 $schema_o->parse($entry);
188 * Return a hash of entries for the given type
190 * Returns a hash of entry for the givene type. Types may be:
191 * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules,
192 * matchingruleuses, nameforms, syntaxes
194 * @param string $type Type to fetch
197 * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error
199 public function &getAll($type)
201 $map = array('objectclasses' => &$this->_objectClasses,
202 'attributes' => &$this->_attributeTypes,
203 'ditcontentrules' => &$this->_dITContentRules,
204 'ditstructurerules' => &$this->_dITStructureRules,
205 'matchingrules' => &$this->_matchingRules,
206 'matchingruleuses' => &$this->_matchingRuleUse,
207 'nameforms' => &$this->_nameForms,
208 'syntaxes' => &$this->_ldapSyntaxes );
210 $key = strtolower($type);
211 $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type"));
216 * Return a specific entry
218 * @param string $type Type of name
219 * @param string $name Name or OID to fetch
222 * @return mixed Entry or Net_LDAP2_Error
224 public function &get($type, $name)
226 if ($this->_initialized) {
227 $type = strtolower($type);
228 if (false == key_exists($type, $this->types)) {
229 return PEAR::raiseError("No such type $type");
232 $name = strtolower($name);
233 $type_var = &$this->{'_' . $this->types[$type]};
235 if (key_exists($name, $type_var)) {
236 return $type_var[$name];
237 } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) {
238 return $this->_oids[$name];
240 return PEAR::raiseError("Could not find $type $name");
250 * Fetches attributes that MAY be present in the given objectclass
252 * @param string $oc Name or OID of objectclass
255 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
257 public function may($oc)
259 return $this->_getAttr($oc, 'may');
263 * Fetches attributes that MUST be present in the given objectclass
265 * @param string $oc Name or OID of objectclass
268 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
270 public function must($oc)
272 return $this->_getAttr($oc, 'must');
276 * Fetches the given attribute from the given objectclass
278 * @param string $oc Name or OID of objectclass
279 * @param string $attr Name of attribute to fetch
282 * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error
284 protected function _getAttr($oc, $attr)
286 $oc = strtolower($oc);
287 if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) {
288 return $this->_objectClasses[$oc][$attr];
289 } elseif (key_exists($oc, $this->_oids) &&
290 $this->_oids[$oc]['type'] == 'objectclass' &&
291 key_exists($attr, $this->_oids[$oc])) {
292 return $this->_oids[$oc][$attr];
294 return PEAR::raiseError("Could not find $attr attributes for $oc ");
299 * Returns the name(s) of the immediate superclass(es)
301 * @param string $oc Name or OID of objectclass
304 * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error
306 public function superclass($oc)
308 $o = $this->get('objectclass', $oc);
309 if (Net_LDAP2::isError($o)) {
312 return (key_exists('sup', $o) ? $o['sup'] : array());
316 * Parses the schema of the given Subschema entry
318 * @param Net_LDAP2_Entry &$entry Subschema entry
323 public function parse(&$entry)
325 foreach ($this->types as $type => $attr) {
326 // initialize map type to entry
327 $type_var = '_' . $attr;
328 $this->{$type_var} = array();
330 // get values for this type
331 if ($entry->exists($attr)) {
332 $values = $entry->getValue($attr);
333 if (is_array($values)) {
334 foreach ($values as $value) {
336 unset($schema_entry); // this was a real mess without it
338 // get the schema entry
339 $schema_entry = $this->_parse_entry($value);
342 $schema_entry['type'] = $type;
344 // save a ref in $_oids
345 $this->_oids[$schema_entry['oid']] = &$schema_entry;
347 // save refs for all names in type map
348 $names = $schema_entry['aliases'];
349 array_push($names, $schema_entry['name']);
350 foreach ($names as $name) {
351 $this->{$type_var}[strtolower($name)] = &$schema_entry;
357 $this->_initialized = true;
361 * Parses an attribute value into a schema entry
363 * @param string $value Attribute value
366 * @return array|false Schema entry array or false
368 protected function &_parse_entry($value)
370 // tokens that have no value associated
371 $noValue = array('single-value',
374 'no-user-modification',
379 // tokens that can have multiple values
380 $multiValue = array('must', 'may', 'sup');
382 $schema_entry = array('aliases' => array()); // initilization
384 $tokens = $this->_tokenize($value); // get an array of tokens
386 // remove surrounding brackets
387 if ($tokens[0] == '(') array_shift($tokens);
388 if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
390 $schema_entry['oid'] = array_shift($tokens); // first token is the oid
392 // cycle over the tokens until none are left
393 while (count($tokens) > 0) {
394 $token = strtolower(array_shift($tokens));
395 if (in_array($token, $noValue)) {
396 $schema_entry[$token] = 1; // single value token
398 // this one follows a string or a list if it is multivalued
399 if (($schema_entry[$token] = array_shift($tokens)) == '(') {
400 // this creates the list of values and cycles through the tokens
401 // until the end of the list is reached ')'
402 $schema_entry[$token] = array();
403 while ($tmp = array_shift($tokens)) {
404 if ($tmp == ')') break;
405 if ($tmp != '$') array_push($schema_entry[$token], $tmp);
408 // create a array if the value should be multivalued but was not
409 if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) {
410 $schema_entry[$token] = array($schema_entry[$token]);
414 // get max length from syntax
415 if (key_exists('syntax', $schema_entry)) {
416 if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
417 $schema_entry['max_length'] = $matches[1];
421 if (empty($schema_entry['name'])) {
422 $schema_entry['name'] = $schema_entry['oid'];
424 // make one name the default and put the other ones into aliases
425 if (is_array($schema_entry['name'])) {
426 $aliases = $schema_entry['name'];
427 $schema_entry['name'] = array_shift($aliases);
428 $schema_entry['aliases'] = $aliases;
430 return $schema_entry;
434 * Tokenizes the given value into an array of tokens
436 * @param string $value String to parse
439 * @return array Array of tokens
441 protected function _tokenize($value)
443 $tokens = array(); // array of tokens
444 $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns
446 // this one is taken from perl-ldap, modified for php
447 $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
450 * This one matches one big pattern wherin only one of the three subpatterns matched
451 * We are interested in the subpatterns that matched. If it matched its value will be
452 * non-empty and so it is a token. Tokens may be round brackets, a string, or a string
455 preg_match_all($pattern, $value, $matches);
457 for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match)
458 for ($j = 1; $j < 4; $j++) { // each subpattern
459 if (null != trim($matches[$j][$i])) { // pattern match in this subpattern
460 $tokens[$i] = trim($matches[$j][$i]); // this is the token
468 * Returns wether a attribute syntax is binary or not
470 * This method gets used by Net_LDAP2_Entry to decide which
471 * PHP function needs to be used to fetch the value in the
472 * proper format (e.g. binary or string)
474 * @param string $attribute The name of the attribute (eg.: 'sn')
479 public function isBinary($attribute)
481 $return = false; // default to false
483 // This list contains all syntax that should be treaten as
484 // containing binary values
485 // The Syntax Definitons go into constants at the top of this page
486 $syntax_binary = array(
487 NET_LDAP2_SYNTAX_OCTET_STRING,
488 NET_LDAP2_SYNTAX_JPEG
492 $attr_s = $this->get('attribute', $attribute);
493 if (Net_LDAP2::isError($attr_s)) {
494 // Attribute not found in schema
495 $return = false; // consider attr not binary
496 } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) {
497 // Syntax is defined as binary in schema
500 // Syntax not defined as binary, or not found
501 // if attribute is a subtype, check superior attribute syntaxes
502 if (isset($attr_s['sup'])) {
503 foreach ($attr_s['sup'] as $superattr) {
504 $return = $this->isBinary($superattr);
506 break; // stop checking parents since we are binary
516 * See if an schema element exists
518 * @param string $type Type of name, see get()
519 * @param string $name Name or OID
523 public function exists($type, $name)
525 $entry = $this->get($type, $name);
526 if ($entry instanceof Net_LDAP2_ERROR) {
534 * See if an attribute is defined in the schema
536 * @param string $attribute Name or OID of the attribute
539 public function attributeExists($attribute)
541 return $this->exists('attribute', $attribute);
545 * See if an objectClass is defined in the schema
547 * @param string $ocl Name or OID of the objectClass
550 public function objectClassExists($ocl)
552 return $this->exists('objectclass', $ocl);
557 * See to which ObjectClasses an attribute is assigned
559 * The objectclasses are sorted into the keys 'may' and 'must'.
561 * @param string $attribute Name or OID of the attribute
563 * @return array|Net_LDAP2_Error Associative array with OCL names or Error
565 public function getAssignedOCLs($attribute)
570 // Test if the attribute type is defined in the schema,
571 // if so, retrieve real name for lookups
572 $attr_entry = $this->get('attribute', $attribute);
573 if ($attr_entry instanceof Net_LDAP2_ERROR) {
574 return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage());
576 $attribute = $attr_entry['name'];
580 // We need to get all defined OCLs for this.
581 $ocls = $this->getAll('objectclasses');
582 foreach ($ocls as $ocl => $ocl_data) {
583 // Fetch the may and must attrs and see if our searched attr is contained.
584 // If so, record it in the corresponding array.
585 $ocl_may_attrs = $this->may($ocl);
586 $ocl_must_attrs = $this->must($ocl);
587 if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
588 array_push($may, $ocl_data['name']);
590 if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
591 array_push($must, $ocl_data['name']);
595 return array('may' => $may, 'must' => $must);
599 * See if an attribute is available in a set of objectClasses
601 * @param string $attribute Attribute name or OID
602 * @param array $ocls Names of OCLs to check for
604 * @return boolean TRUE, if the attribute is defined for at least one of the OCLs
606 public function checkAttribute($attribute, $ocls)
608 foreach ($ocls as $ocl) {
609 $ocl_entry = $this->get('objectclass', $ocl);
610 $ocl_may_attrs = $this->may($ocl);
611 $ocl_must_attrs = $this->must($ocl);
612 if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
615 if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
619 return false; // no ocl for the ocls found.