1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4: */ 3/** 4* File containing the Net_LDAP2_Schema interface class. 5* 6* PHP version 5 7* 8* @category Net 9* @package Net_LDAP2 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$ 15* @link http://pear.php.net/package/Net_LDAP2/ 16* @todo see the comment at the end of the file 17*/ 18 19/** 20* Includes 21*/ 22require_once 'PEAR.php'; 23 24/** 25* Syntax definitions 26* 27* Please don't forget to add binary attributes to isBinary() below 28* to support proper value fetching from Net_LDAP2_Entry 29*/ 30define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7'); 31define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15'); 32define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12'); 33define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27'); 34define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28'); 35define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36'); 36define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38'); 37define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40'); 38 39/** 40* Load an LDAP Schema and provide information 41* 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. 46* 47* @category Net 48* @package Net_LDAP2 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/ 53*/ 54class Net_LDAP2_Schema extends PEAR 55{ 56 /** 57 * Map of entry types to ldap attributes of subschema entry 58 * 59 * @access public 60 * @var array 61 */ 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' 71 ); 72 73 /** 74 * Array of entries belonging to this type 75 * 76 * @access protected 77 * @var array 78 */ 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(); 87 88 89 /** 90 * hash of all fetched oids 91 * 92 * @access protected 93 * @var array 94 */ 95 protected $_oids = array(); 96 97 /** 98 * Tells if the schema is initialized 99 * 100 * @access protected 101 * @var boolean 102 * @see parse(), get() 103 */ 104 protected $_initialized = false; 105 106 107 /** 108 * Constructor of the class 109 * 110 * @access protected 111 */ 112 public function __construct() 113 { 114 parent::__construct('Net_LDAP2_Error'); // default error class 115 } 116 117 /** 118 * Fetch the Schema from an LDAP connection 119 * 120 * @param Net_LDAP2 $ldap LDAP connection 121 * @param string $dn (optional) Subschema entry dn 122 * 123 * @access public 124 * @return Net_LDAP2_Schema|NET_LDAP2_Error 125 */ 126 public static function fetch($ldap, $dn = null) 127 { 128 if (!$ldap instanceof Net_LDAP2) { 129 return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!"); 130 } 131 132 $schema_o = new Net_LDAP2_Schema(); 133 134 if (is_null($dn)) { 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)) { 140 $dn = $base; 141 } 142 } 143 } 144 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. 148 if (is_null($dn)) { 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)) { 154 $dn = $base; 155 } 156 } 157 } 158 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). 162 if (is_null($dn)) { 163 $dn = 'cn=Subschema'; 164 } 165 166 // fetch the subschema entry 167 $result = $ldap->search($dn, '(objectClass=*)', 168 array('attributes' => array_values($schema_o->types), 169 'scope' => 'base')); 170 if (Net_LDAP2::isError($result)) { 171 return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage()); 172 } 173 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()); 178 } else { 179 return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')'); 180 } 181 } 182 183 $schema_o->parse($entry); 184 return $schema_o; 185 } 186 187 /** 188 * Return a hash of entries for the given type 189 * 190 * Returns a hash of entry for the givene type. Types may be: 191 * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules, 192 * matchingruleuses, nameforms, syntaxes 193 * 194 * @param string $type Type to fetch 195 * 196 * @access public 197 * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error 198 */ 199 public function &getAll($type) 200 { 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 ); 209 210 $key = strtolower($type); 211 $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type")); 212 return $ret; 213 } 214 215 /** 216 * Return a specific entry 217 * 218 * @param string $type Type of name 219 * @param string $name Name or OID to fetch 220 * 221 * @access public 222 * @return mixed Entry or Net_LDAP2_Error 223 */ 224 public function &get($type, $name) 225 { 226 if ($this->_initialized) { 227 $type = strtolower($type); 228 if (false == key_exists($type, $this->types)) { 229 return PEAR::raiseError("No such type $type"); 230 } 231 232 $name = strtolower($name); 233 $type_var = &$this->{'_' . $this->types[$type]}; 234 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]; 239 } else { 240 return PEAR::raiseError("Could not find $type $name"); 241 } 242 } else { 243 $return = null; 244 return $return; 245 } 246 } 247 248 249 /** 250 * Fetches attributes that MAY be present in the given objectclass 251 * 252 * @param string $oc Name or OID of objectclass 253 * 254 * @access public 255 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error 256 */ 257 public function may($oc) 258 { 259 return $this->_getAttr($oc, 'may'); 260 } 261 262 /** 263 * Fetches attributes that MUST be present in the given objectclass 264 * 265 * @param string $oc Name or OID of objectclass 266 * 267 * @access public 268 * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error 269 */ 270 public function must($oc) 271 { 272 return $this->_getAttr($oc, 'must'); 273 } 274 275 /** 276 * Fetches the given attribute from the given objectclass 277 * 278 * @param string $oc Name or OID of objectclass 279 * @param string $attr Name of attribute to fetch 280 * 281 * @access protected 282 * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error 283 */ 284 protected function _getAttr($oc, $attr) 285 { 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]; 293 } else { 294 return PEAR::raiseError("Could not find $attr attributes for $oc "); 295 } 296 } 297 298 /** 299 * Returns the name(s) of the immediate superclass(es) 300 * 301 * @param string $oc Name or OID of objectclass 302 * 303 * @access public 304 * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error 305 */ 306 public function superclass($oc) 307 { 308 $o = $this->get('objectclass', $oc); 309 if (Net_LDAP2::isError($o)) { 310 return $o; 311 } 312 return (key_exists('sup', $o) ? $o['sup'] : array()); 313 } 314 315 /** 316 * Parses the schema of the given Subschema entry 317 * 318 * @param Net_LDAP2_Entry &$entry Subschema entry 319 * 320 * @access public 321 * @return void 322 */ 323 public function parse(&$entry) 324 { 325 foreach ($this->types as $type => $attr) { 326 // initialize map type to entry 327 $type_var = '_' . $attr; 328 $this->{$type_var} = array(); 329 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) { 335 336 unset($schema_entry); // this was a real mess without it 337 338 // get the schema entry 339 $schema_entry = $this->_parse_entry($value); 340 341 // set the type 342 $schema_entry['type'] = $type; 343 344 // save a ref in $_oids 345 $this->_oids[$schema_entry['oid']] = &$schema_entry; 346 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; 352 } 353 } 354 } 355 } 356 } 357 $this->_initialized = true; 358 } 359 360 /** 361 * Parses an attribute value into a schema entry 362 * 363 * @param string $value Attribute value 364 * 365 * @access protected 366 * @return array|false Schema entry array or false 367 */ 368 protected function &_parse_entry($value) 369 { 370 // tokens that have no value associated 371 $noValue = array('single-value', 372 'obsolete', 373 'collective', 374 'no-user-modification', 375 'abstract', 376 'structural', 377 'auxiliary'); 378 379 // tokens that can have multiple values 380 $multiValue = array('must', 'may', 'sup'); 381 382 $schema_entry = array('aliases' => array()); // initilization 383 384 $tokens = $this->_tokenize($value); // get an array of tokens 385 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 :-( 389 390 $schema_entry['oid'] = array_shift($tokens); // first token is the oid 391 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 397 } else { 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); 406 } 407 } 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]); 411 } 412 } 413 } 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]; 418 } 419 } 420 // force a name 421 if (empty($schema_entry['name'])) { 422 $schema_entry['name'] = $schema_entry['oid']; 423 } 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; 429 } 430 return $schema_entry; 431 } 432 433 /** 434 * Tokenizes the given value into an array of tokens 435 * 436 * @param string $value String to parse 437 * 438 * @access protected 439 * @return array Array of tokens 440 */ 441 protected function _tokenize($value) 442 { 443 $tokens = array(); // array of tokens 444 $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns 445 446 // this one is taken from perl-ldap, modified for php 447 $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x"; 448 449 /** 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 453 * enclosed by ' 454 */ 455 preg_match_all($pattern, $value, $matches); 456 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 461 } 462 } 463 } 464 return $tokens; 465 } 466 467 /** 468 * Returns wether a attribute syntax is binary or not 469 * 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) 473 * 474 * @param string $attribute The name of the attribute (eg.: 'sn') 475 * 476 * @access public 477 * @return boolean 478 */ 479 public function isBinary($attribute) 480 { 481 $return = false; // default to false 482 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 489 ); 490 491 // Check Syntax 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 498 $return = true; 499 } else { 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); 505 if ($return) { 506 break; // stop checking parents since we are binary 507 } 508 } 509 } 510 } 511 512 return $return; 513 } 514 515 /** 516 * See if an schema element exists 517 * 518 * @param string $type Type of name, see get() 519 * @param string $name Name or OID 520 * 521 * @return boolean 522 */ 523 public function exists($type, $name) 524 { 525 $entry = $this->get($type, $name); 526 if ($entry instanceof Net_LDAP2_ERROR) { 527 return false; 528 } else { 529 return true; 530 } 531 } 532 533 /** 534 * See if an attribute is defined in the schema 535 * 536 * @param string $attribute Name or OID of the attribute 537 * @return boolean 538 */ 539 public function attributeExists($attribute) 540 { 541 return $this->exists('attribute', $attribute); 542 } 543 544 /** 545 * See if an objectClass is defined in the schema 546 * 547 * @param string $ocl Name or OID of the objectClass 548 * @return boolean 549 */ 550 public function objectClassExists($ocl) 551 { 552 return $this->exists('objectclass', $ocl); 553 } 554 555 556 /** 557 * See to which ObjectClasses an attribute is assigned 558 * 559 * The objectclasses are sorted into the keys 'may' and 'must'. 560 * 561 * @param string $attribute Name or OID of the attribute 562 * 563 * @return array|Net_LDAP2_Error Associative array with OCL names or Error 564 */ 565 public function getAssignedOCLs($attribute) 566 { 567 $may = array(); 568 $must = array(); 569 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()); 575 } else { 576 $attribute = $attr_entry['name']; 577 } 578 579 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']); 589 } 590 if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { 591 array_push($must, $ocl_data['name']); 592 } 593 } 594 595 return array('may' => $may, 'must' => $must); 596 } 597 598 /** 599 * See if an attribute is available in a set of objectClasses 600 * 601 * @param string $attribute Attribute name or OID 602 * @param array $ocls Names of OCLs to check for 603 * 604 * @return boolean TRUE, if the attribute is defined for at least one of the OCLs 605 */ 606 public function checkAttribute($attribute, $ocls) 607 { 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)) { 613 return true; 614 } 615 if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { 616 return true; 617 } 618 } 619 return false; // no ocl for the ocls found. 620 } 621} 622?>