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-2017 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 */ 20class 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 $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 $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