Schema.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  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. * Includes
  20. */
  21. require_once 'PEAR.php';
  22. /**
  23. * Syntax definitions
  24. *
  25. * Please don't forget to add binary attributes to isBinary() below
  26. * to support proper value fetching from Net_LDAP2_Entry
  27. */
  28. define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7');
  29. define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15');
  30. define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12');
  31. define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27');
  32. define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28');
  33. define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36');
  34. define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38');
  35. define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40');
  36. /**
  37. * Load an LDAP Schema and provide information
  38. *
  39. * This class takes a Subschema entry, parses this information
  40. * and makes it available in an array. Most of the code has been
  41. * inspired by perl-ldap( http://perl-ldap.sourceforge.net).
  42. * You will find portions of their implementation in here.
  43. *
  44. * @category Net
  45. * @package Net_LDAP2
  46. * @author Jan Wagner <wagner@netsols.de>
  47. * @author Benedikt Hallinger <beni@php.net>
  48. * @license http://www.gnu.org/copyleft/lesser.html LGPL
  49. * @link http://pear.php.net/package/Net_LDAP22/
  50. */
  51. class Net_LDAP2_Schema extends PEAR
  52. {
  53. /**
  54. * Map of entry types to ldap attributes of subschema entry
  55. *
  56. * @access public
  57. * @var array
  58. */
  59. public $types = array(
  60. 'attribute' => 'attributeTypes',
  61. 'ditcontentrule' => 'dITContentRules',
  62. 'ditstructurerule' => 'dITStructureRules',
  63. 'matchingrule' => 'matchingRules',
  64. 'matchingruleuse' => 'matchingRuleUse',
  65. 'nameform' => 'nameForms',
  66. 'objectclass' => 'objectClasses',
  67. 'syntax' => 'ldapSyntaxes'
  68. );
  69. /**
  70. * Array of entries belonging to this type
  71. *
  72. * @access protected
  73. * @var array
  74. */
  75. protected $_attributeTypes = array();
  76. protected $_matchingRules = array();
  77. protected $_matchingRuleUse = array();
  78. protected $_ldapSyntaxes = array();
  79. protected $_objectClasses = array();
  80. protected $_dITContentRules = array();
  81. protected $_dITStructureRules = array();
  82. protected $_nameForms = array();
  83. /**
  84. * hash of all fetched oids
  85. *
  86. * @access protected
  87. * @var array
  88. */
  89. protected $_oids = array();
  90. /**
  91. * Tells if the schema is initialized
  92. *
  93. * @access protected
  94. * @var boolean
  95. * @see parse(), get()
  96. */
  97. protected $_initialized = false;
  98. /**
  99. * Constructor of the class
  100. *
  101. * @access protected
  102. */
  103. public function __construct()
  104. {
  105. parent::__construct('Net_LDAP2_Error'); // default error class
  106. }
  107. /**
  108. * Fetch the Schema from an LDAP connection
  109. *
  110. * @param Net_LDAP2 $ldap LDAP connection
  111. * @param string $dn (optional) Subschema entry dn
  112. *
  113. * @access public
  114. * @return Net_LDAP2_Schema|NET_LDAP2_Error
  115. */
  116. public static function fetch($ldap, $dn = null)
  117. {
  118. if (!$ldap instanceof Net_LDAP2) {
  119. return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!");
  120. }
  121. $schema_o = new Net_LDAP2_Schema();
  122. if (is_null($dn)) {
  123. // get the subschema entry via root dse
  124. $dse = $ldap->rootDSE(array('subschemaSubentry'));
  125. if (false == Net_LDAP2::isError($dse)) {
  126. $base = $dse->getValue('subschemaSubentry', 'single');
  127. if (!Net_LDAP2::isError($base)) {
  128. $dn = $base;
  129. }
  130. }
  131. }
  132. // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly
  133. // call this entry subSchemaSubentry instead of subschemaSubentry.
  134. // Note the correct case/spelling as per RFC 2251.
  135. if (is_null($dn)) {
  136. // get the subschema entry via root dse
  137. $dse = $ldap->rootDSE(array('subSchemaSubentry'));
  138. if (false == Net_LDAP2::isError($dse)) {
  139. $base = $dse->getValue('subSchemaSubentry', 'single');
  140. if (!Net_LDAP2::isError($base)) {
  141. $dn = $base;
  142. }
  143. }
  144. }
  145. // Final fallback case where there is no subschemaSubentry attribute
  146. // in the root DSE (this is a bug for an LDAP v3 server so report this
  147. // to your LDAP vendor if you get this far).
  148. if (is_null($dn)) {
  149. $dn = 'cn=Subschema';
  150. }
  151. // fetch the subschema entry
  152. $result = $ldap->search($dn, '(objectClass=*)',
  153. array('attributes' => array_values($schema_o->types),
  154. 'scope' => 'base'));
  155. if (Net_LDAP2::isError($result)) {
  156. return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage());
  157. }
  158. $entry = $result->shiftEntry();
  159. if (!$entry instanceof Net_LDAP2_Entry) {
  160. if ($entry instanceof Net_LDAP2_Error) {
  161. return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage());
  162. } else {
  163. return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')');
  164. }
  165. }
  166. $schema_o->parse($entry);
  167. return $schema_o;
  168. }
  169. /**
  170. * Return a hash of entries for the given type
  171. *
  172. * Returns a hash of entry for the givene type. Types may be:
  173. * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules,
  174. * matchingruleuses, nameforms, syntaxes
  175. *
  176. * @param string $type Type to fetch
  177. *
  178. * @access public
  179. * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error
  180. */
  181. public function &getAll($type)
  182. {
  183. $map = array('objectclasses' => &$this->_objectClasses,
  184. 'attributes' => &$this->_attributeTypes,
  185. 'ditcontentrules' => &$this->_dITContentRules,
  186. 'ditstructurerules' => &$this->_dITStructureRules,
  187. 'matchingrules' => &$this->_matchingRules,
  188. 'matchingruleuses' => &$this->_matchingRuleUse,
  189. 'nameforms' => &$this->_nameForms,
  190. 'syntaxes' => &$this->_ldapSyntaxes );
  191. $key = strtolower($type);
  192. $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type"));
  193. return $ret;
  194. }
  195. /**
  196. * Return a specific entry
  197. *
  198. * @param string $type Type of name
  199. * @param string $name Name or OID to fetch
  200. *
  201. * @access public
  202. * @return mixed Entry or Net_LDAP2_Error
  203. */
  204. public function &get($type, $name)
  205. {
  206. if ($this->_initialized) {
  207. $type = strtolower($type);
  208. if (false == key_exists($type, $this->types)) {
  209. return PEAR::raiseError("No such type $type");
  210. }
  211. $name = strtolower($name);
  212. $type_var = &$this->{'_' . $this->types[$type]};
  213. if (key_exists($name, $type_var)) {
  214. return $type_var[$name];
  215. } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) {
  216. return $this->_oids[$name];
  217. } else {
  218. return PEAR::raiseError("Could not find $type $name");
  219. }
  220. } else {
  221. $return = null;
  222. return $return;
  223. }
  224. }
  225. /**
  226. * Fetches attributes that MAY be present in the given objectclass
  227. *
  228. * @param string $oc Name or OID of objectclass
  229. *
  230. * @access public
  231. * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
  232. */
  233. public function may($oc)
  234. {
  235. return $this->_getAttr($oc, 'may');
  236. }
  237. /**
  238. * Fetches attributes that MUST be present in the given objectclass
  239. *
  240. * @param string $oc Name or OID of objectclass
  241. *
  242. * @access public
  243. * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
  244. */
  245. public function must($oc)
  246. {
  247. return $this->_getAttr($oc, 'must');
  248. }
  249. /**
  250. * Fetches the given attribute from the given objectclass
  251. *
  252. * @param string $oc Name or OID of objectclass
  253. * @param string $attr Name of attribute to fetch
  254. *
  255. * @access protected
  256. * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error
  257. */
  258. protected function _getAttr($oc, $attr)
  259. {
  260. $oc = strtolower($oc);
  261. if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) {
  262. return $this->_objectClasses[$oc][$attr];
  263. } elseif (key_exists($oc, $this->_oids) &&
  264. $this->_oids[$oc]['type'] == 'objectclass' &&
  265. key_exists($attr, $this->_oids[$oc])) {
  266. return $this->_oids[$oc][$attr];
  267. } else {
  268. return PEAR::raiseError("Could not find $attr attributes for $oc ");
  269. }
  270. }
  271. /**
  272. * Returns the name(s) of the immediate superclass(es)
  273. *
  274. * @param string $oc Name or OID of objectclass
  275. *
  276. * @access public
  277. * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error
  278. */
  279. public function superclass($oc)
  280. {
  281. $o = $this->get('objectclass', $oc);
  282. if (Net_LDAP2::isError($o)) {
  283. return $o;
  284. }
  285. return (key_exists('sup', $o) ? $o['sup'] : array());
  286. }
  287. /**
  288. * Parses the schema of the given Subschema entry
  289. *
  290. * @param Net_LDAP2_Entry &$entry Subschema entry
  291. *
  292. * @access public
  293. * @return void
  294. */
  295. public function parse(&$entry)
  296. {
  297. foreach ($this->types as $type => $attr) {
  298. // initialize map type to entry
  299. $type_var = '_' . $attr;
  300. $this->{$type_var} = array();
  301. // get values for this type
  302. if ($entry->exists($attr)) {
  303. $values = $entry->getValue($attr);
  304. if (is_array($values)) {
  305. foreach ($values as $value) {
  306. unset($schema_entry); // this was a real mess without it
  307. // get the schema entry
  308. $schema_entry = $this->_parse_entry($value);
  309. // set the type
  310. $schema_entry['type'] = $type;
  311. // save a ref in $_oids
  312. $this->_oids[$schema_entry['oid']] = &$schema_entry;
  313. // save refs for all names in type map
  314. $names = $schema_entry['aliases'];
  315. array_push($names, $schema_entry['name']);
  316. foreach ($names as $name) {
  317. $this->{$type_var}[strtolower($name)] = &$schema_entry;
  318. }
  319. }
  320. }
  321. }
  322. }
  323. $this->_initialized = true;
  324. }
  325. /**
  326. * Parses an attribute value into a schema entry
  327. *
  328. * @param string $value Attribute value
  329. *
  330. * @access protected
  331. * @return array|false Schema entry array or false
  332. */
  333. protected function &_parse_entry($value)
  334. {
  335. // tokens that have no value associated
  336. $noValue = array('single-value',
  337. 'obsolete',
  338. 'collective',
  339. 'no-user-modification',
  340. 'abstract',
  341. 'structural',
  342. 'auxiliary');
  343. // tokens that can have multiple values
  344. $multiValue = array('must', 'may', 'sup');
  345. $schema_entry = array('aliases' => array()); // initilization
  346. $tokens = $this->_tokenize($value); // get an array of tokens
  347. // remove surrounding brackets
  348. if ($tokens[0] == '(') array_shift($tokens);
  349. if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
  350. $schema_entry['oid'] = array_shift($tokens); // first token is the oid
  351. // cycle over the tokens until none are left
  352. while (count($tokens) > 0) {
  353. $token = strtolower(array_shift($tokens));
  354. if (in_array($token, $noValue)) {
  355. $schema_entry[$token] = 1; // single value token
  356. } else {
  357. // this one follows a string or a list if it is multivalued
  358. if (($schema_entry[$token] = array_shift($tokens)) == '(') {
  359. // this creates the list of values and cycles through the tokens
  360. // until the end of the list is reached ')'
  361. $schema_entry[$token] = array();
  362. while ($tmp = array_shift($tokens)) {
  363. if ($tmp == ')') break;
  364. if ($tmp != '$') array_push($schema_entry[$token], $tmp);
  365. }
  366. }
  367. // create a array if the value should be multivalued but was not
  368. if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) {
  369. $schema_entry[$token] = array($schema_entry[$token]);
  370. }
  371. }
  372. }
  373. // get max length from syntax
  374. if (key_exists('syntax', $schema_entry)) {
  375. if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
  376. $schema_entry['max_length'] = $matches[1];
  377. }
  378. }
  379. // force a name
  380. if (empty($schema_entry['name'])) {
  381. $schema_entry['name'] = $schema_entry['oid'];
  382. }
  383. // make one name the default and put the other ones into aliases
  384. if (is_array($schema_entry['name'])) {
  385. $aliases = $schema_entry['name'];
  386. $schema_entry['name'] = array_shift($aliases);
  387. $schema_entry['aliases'] = $aliases;
  388. }
  389. return $schema_entry;
  390. }
  391. /**
  392. * Tokenizes the given value into an array of tokens
  393. *
  394. * @param string $value String to parse
  395. *
  396. * @access protected
  397. * @return array Array of tokens
  398. */
  399. protected function _tokenize($value)
  400. {
  401. $tokens = array(); // array of tokens
  402. $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns
  403. // this one is taken from perl-ldap, modified for php
  404. $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
  405. /**
  406. * This one matches one big pattern wherin only one of the three subpatterns matched
  407. * We are interested in the subpatterns that matched. If it matched its value will be
  408. * non-empty and so it is a token. Tokens may be round brackets, a string, or a string
  409. * enclosed by '
  410. */
  411. preg_match_all($pattern, $value, $matches);
  412. for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match)
  413. for ($j = 1; $j < 4; $j++) { // each subpattern
  414. if (null != trim($matches[$j][$i])) { // pattern match in this subpattern
  415. $tokens[$i] = trim($matches[$j][$i]); // this is the token
  416. }
  417. }
  418. }
  419. return $tokens;
  420. }
  421. /**
  422. * Returns wether a attribute syntax is binary or not
  423. *
  424. * This method gets used by Net_LDAP2_Entry to decide which
  425. * PHP function needs to be used to fetch the value in the
  426. * proper format (e.g. binary or string)
  427. *
  428. * @param string $attribute The name of the attribute (eg.: 'sn')
  429. *
  430. * @access public
  431. * @return boolean
  432. */
  433. public function isBinary($attribute)
  434. {
  435. $return = false; // default to false
  436. // This list contains all syntax that should be treaten as
  437. // containing binary values
  438. // The Syntax Definitons go into constants at the top of this page
  439. $syntax_binary = array(
  440. NET_LDAP2_SYNTAX_OCTET_STRING,
  441. NET_LDAP2_SYNTAX_JPEG
  442. );
  443. // Check Syntax
  444. $attr_s = $this->get('attribute', $attribute);
  445. if (Net_LDAP2::isError($attr_s)) {
  446. // Attribute not found in schema
  447. $return = false; // consider attr not binary
  448. } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) {
  449. // Syntax is defined as binary in schema
  450. $return = true;
  451. } else {
  452. // Syntax not defined as binary, or not found
  453. // if attribute is a subtype, check superior attribute syntaxes
  454. if (isset($attr_s['sup'])) {
  455. foreach ($attr_s['sup'] as $superattr) {
  456. $return = $this->isBinary($superattr);
  457. if ($return) {
  458. break; // stop checking parents since we are binary
  459. }
  460. }
  461. }
  462. }
  463. return $return;
  464. }
  465. /**
  466. * See if an schema element exists
  467. *
  468. * @param string $type Type of name, see get()
  469. * @param string $name Name or OID
  470. *
  471. * @return boolean
  472. */
  473. public function exists($type, $name)
  474. {
  475. $entry = $this->get($type, $name);
  476. if ($entry instanceof Net_LDAP2_ERROR) {
  477. return false;
  478. } else {
  479. return true;
  480. }
  481. }
  482. /**
  483. * See if an attribute is defined in the schema
  484. *
  485. * @param string $attribute Name or OID of the attribute
  486. * @return boolean
  487. */
  488. public function attributeExists($attribute)
  489. {
  490. return $this->exists('attribute', $attribute);
  491. }
  492. /**
  493. * See if an objectClass is defined in the schema
  494. *
  495. * @param string $ocl Name or OID of the objectClass
  496. * @return boolean
  497. */
  498. public function objectClassExists($ocl)
  499. {
  500. return $this->exists('objectclass', $ocl);
  501. }
  502. /**
  503. * See to which ObjectClasses an attribute is assigned
  504. *
  505. * The objectclasses are sorted into the keys 'may' and 'must'.
  506. *
  507. * @param string $attribute Name or OID of the attribute
  508. *
  509. * @return array|Net_LDAP2_Error Associative array with OCL names or Error
  510. */
  511. public function getAssignedOCLs($attribute)
  512. {
  513. $may = array();
  514. $must = array();
  515. // Test if the attribute type is defined in the schema,
  516. // if so, retrieve real name for lookups
  517. $attr_entry = $this->get('attribute', $attribute);
  518. if ($attr_entry instanceof Net_LDAP2_ERROR) {
  519. return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage());
  520. } else {
  521. $attribute = $attr_entry['name'];
  522. }
  523. // We need to get all defined OCLs for this.
  524. $ocls = $this->getAll('objectclasses');
  525. foreach ($ocls as $ocl => $ocl_data) {
  526. // Fetch the may and must attrs and see if our searched attr is contained.
  527. // If so, record it in the corresponding array.
  528. $ocl_may_attrs = $this->may($ocl);
  529. $ocl_must_attrs = $this->must($ocl);
  530. if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
  531. array_push($may, $ocl_data['name']);
  532. }
  533. if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
  534. array_push($must, $ocl_data['name']);
  535. }
  536. }
  537. return array('may' => $may, 'must' => $must);
  538. }
  539. /**
  540. * See if an attribute is available in a set of objectClasses
  541. *
  542. * @param string $attribute Attribute name or OID
  543. * @param array $ocls Names of OCLs to check for
  544. *
  545. * @return boolean TRUE, if the attribute is defined for at least one of the OCLs
  546. */
  547. public function checkAttribute($attribute, $ocls)
  548. {
  549. foreach ($ocls as $ocl) {
  550. $ocl_entry = $this->get('objectclass', $ocl);
  551. $ocl_may_attrs = $this->may($ocl);
  552. $ocl_must_attrs = $this->must($ocl);
  553. if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
  554. return true;
  555. }
  556. if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
  557. return true;
  558. }
  559. }
  560. return false; // no ocl for the ocls found.
  561. }
  562. }
  563. ?>