488 lines
16 KiB
PHP
488 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Load an LDAP Schema and provide information
|
|
*
|
|
* This class takes a Subschema entry, parses this information
|
|
* and makes it available in an array. Most of the code has been
|
|
* inspired by perl-ldap( http://perl-ldap.sourceforge.net).
|
|
* You will find portions of their implementation in here.
|
|
*
|
|
* Copyright 2009 Jan Wagner, Benedikt Hallinger
|
|
* Copyright 2010-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* @category Horde
|
|
* @package Ldap
|
|
* @author Jan Wagner <wagner@netsols.de>
|
|
* @author Benedikt Hallinger <beni@php.net>
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
|
|
*/
|
|
class Horde_Ldap_Schema
|
|
{
|
|
/**
|
|
* Syntax definitions.
|
|
*
|
|
* Please don't forget to add binary attributes to isBinary() below to
|
|
* support proper value fetching from Horde_Ldap_Entry.
|
|
*/
|
|
const SYNTAX_BOOLEAN = '1.3.6.1.4.1.1466.115.121.1.7';
|
|
const SYNTAX_DIRECTORY_STRING = '1.3.6.1.4.1.1466.115.121.1.15';
|
|
const SYNTAX_DISTINGUISHED_NAME = '1.3.6.1.4.1.1466.115.121.1.12';
|
|
const SYNTAX_INTEGER = '1.3.6.1.4.1.1466.115.121.1.27';
|
|
const SYNTAX_JPEG = '1.3.6.1.4.1.1466.115.121.1.28';
|
|
const SYNTAX_NUMERIC_STRING = '1.3.6.1.4.1.1466.115.121.1.36';
|
|
const SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.38';
|
|
const SYNTAX_OCTET_STRING = '1.3.6.1.4.1.1466.115.121.1.40';
|
|
|
|
/**
|
|
* Map of entry types to LDAP attributes of subschema entry.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $types = array(
|
|
'attribute' => 'attributeTypes',
|
|
'ditcontentrule' => 'dITContentRules',
|
|
'ditstructurerule' => 'dITStructureRules',
|
|
'matchingrule' => 'matchingRules',
|
|
'matchingruleuse' => 'matchingRuleUse',
|
|
'nameform' => 'nameForms',
|
|
'objectclass' => 'objectClasses',
|
|
'syntax' => 'ldapSyntaxes' );
|
|
|
|
/**
|
|
* Array of entries belonging to this type
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_attributeTypes = array();
|
|
protected $_matchingRules = array();
|
|
protected $_matchingRuleUse = array();
|
|
protected $_ldapSyntaxes = array();
|
|
protected $_objectClasses = array();
|
|
protected $_dITContentRules = array();
|
|
protected $_dITStructureRules = array();
|
|
protected $_nameForms = array();
|
|
|
|
|
|
/**
|
|
* Hash of all fetched OIDs.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_oids = array();
|
|
|
|
/**
|
|
* Whether the schema is initialized.
|
|
*
|
|
* @see parse(), get()
|
|
* @var boolean
|
|
*/
|
|
protected $_initialized = false;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* Fetches the Schema from an LDAP connection.
|
|
*
|
|
* @param Horde_Ldap $ldap LDAP connection.
|
|
* @param string $dn Subschema entry DN.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function __construct(Horde_Ldap $ldap, $dn = null)
|
|
{
|
|
if (is_null($dn)) {
|
|
// Get the subschema entry via rootDSE.
|
|
$dse = $ldap->rootDSE(array('subschemaSubentry'));
|
|
$base = $dse->getValue('subschemaSubentry', 'single');
|
|
$dn = $base;
|
|
}
|
|
|
|
// Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that
|
|
// incorrectly call this entry subSchemaSubentry instead of
|
|
// subschemaSubentry. Note the correct case/spelling as per RFC 2251.
|
|
if (is_null($dn)) {
|
|
// Get the subschema entry via rootDSE.
|
|
$dse = $ldap->rootDSE(array('subSchemaSubentry'));
|
|
$base = $dse->getValue('subSchemaSubentry', 'single');
|
|
$dn = $base;
|
|
}
|
|
|
|
// Final fallback in case there is no subschemaSubentry attribute in
|
|
// the root DSE (this is a bug for an LDAPv3 server so report this to
|
|
// your LDAP vendor if you get this far).
|
|
if (is_null($dn)) {
|
|
$dn = 'cn=Subschema';
|
|
}
|
|
|
|
// Fetch the subschema entry.
|
|
$result = $ldap->search($dn, '(objectClass=*)',
|
|
array('attributes' => array_values($this->types),
|
|
'scope' => 'base'));
|
|
$entry = $result->shiftEntry();
|
|
if (!($entry instanceof Horde_Ldap_Entry)) {
|
|
throw new Horde_Ldap_Exception('Could not fetch Subschema entry');
|
|
}
|
|
|
|
$this->parse($entry);
|
|
}
|
|
|
|
/**
|
|
* Returns a hash of entries for the given type.
|
|
*
|
|
* Types may be: objectclasses, attributes, ditcontentrules,
|
|
* ditstructurerules, matchingrules, matchingruleuses, nameforms, syntaxes.
|
|
*
|
|
* @param string $type Type to fetch.
|
|
*
|
|
* @return array
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function getAll($type)
|
|
{
|
|
$map = array('objectclasses' => $this->_objectClasses,
|
|
'attributes' => $this->_attributeTypes,
|
|
'ditcontentrules' => $this->_dITContentRules,
|
|
'ditstructurerules' => $this->_dITStructureRules,
|
|
'matchingrules' => $this->_matchingRules,
|
|
'matchingruleuses' => $this->_matchingRuleUse,
|
|
'nameforms' => $this->_nameForms,
|
|
'syntaxes' => $this->_ldapSyntaxes);
|
|
|
|
$key = Horde_String::lower($type);
|
|
if (!isset($map[$key])) {
|
|
throw new Horde_Ldap_Exception("Unknown type $type");
|
|
}
|
|
|
|
return $map[$key];
|
|
}
|
|
|
|
/**
|
|
* Returns a specific entry.
|
|
*
|
|
* @param string $type Type of name.
|
|
* @param string $name Name or OID to fetch.
|
|
*
|
|
* @return mixed
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function get($type, $name)
|
|
{
|
|
if (!$this->_initialized) {
|
|
return null;
|
|
}
|
|
|
|
$type = Horde_String::lower($type);
|
|
if (!isset($this->types[$type])) {
|
|
throw new Horde_Ldap_Exception("No such type $type");
|
|
}
|
|
|
|
$name = Horde_String::lower($name);
|
|
$type_var = $this->{'_' . $this->types[$type]};
|
|
|
|
if (isset($type_var[$name])) {
|
|
return $type_var[$name];
|
|
}
|
|
if (isset($this->_oids[$name]) &&
|
|
$this->_oids[$name]['type'] == $type) {
|
|
return $this->_oids[$name];
|
|
}
|
|
throw new Horde_Ldap_Exception("Could not find $type $name");
|
|
}
|
|
|
|
|
|
/**
|
|
* Fetches attributes that MAY be present in the given objectclass.
|
|
*
|
|
* @param string $oc Name or OID of objectclass.
|
|
* @param boolean $checksup Check all superiour objectclasses too?
|
|
*
|
|
* @return array Array with attributes.
|
|
*/
|
|
public function may($oc, $checksup = false)
|
|
{
|
|
try {
|
|
$attributes = $this->_getAttr($oc, 'may');
|
|
} catch (Horde_Ldap_Exception $e) {
|
|
$attributes = array();
|
|
}
|
|
if ($checksup) {
|
|
try {
|
|
foreach ($this->superclass($oc) as $sup) {
|
|
$attributes = array_merge($attributes, $this->may($sup, true));
|
|
}
|
|
} catch (Horde_Ldap_Exception $e) {
|
|
}
|
|
$attributes = array_values(array_unique($attributes));
|
|
}
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Fetches attributes that MUST be present in the given objectclass.
|
|
*
|
|
* @param string $oc Name or OID of objectclass.
|
|
* @param boolean $checksup Check all superiour objectclasses too?
|
|
*
|
|
* @return array Array with attributes.
|
|
*/
|
|
public function must($oc, $checksup = false)
|
|
{
|
|
try {
|
|
$attributes = $this->_getAttr($oc, 'must');
|
|
} catch (Horde_Ldap_Exception $e) {
|
|
$attributes = array();
|
|
}
|
|
if ($checksup) {
|
|
try {
|
|
foreach ($this->superclass($oc) as $sup) {
|
|
$attributes = array_merge($attributes, $this->must($sup, true));
|
|
}
|
|
} catch (Horde_Ldap_Exception $e) {
|
|
}
|
|
$attributes = array_values(array_unique($attributes));
|
|
}
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Fetches the given attribute from the given objectclass.
|
|
*
|
|
* @param string $oc Name or OID of objectclass.
|
|
* @param string $attr Name of attribute to fetch.
|
|
*
|
|
* @return array The attribute.
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _getAttr($oc, $attr)
|
|
{
|
|
$oc = Horde_String::lower($oc);
|
|
if (isset($this->_objectClasses[$oc]) &&
|
|
isset($this->_objectClasses[$oc][$attr])) {
|
|
return $this->_objectClasses[$oc][$attr];
|
|
}
|
|
if (isset($this->_oids[$oc]) &&
|
|
$this->_oids[$oc]['type'] == 'objectclass' &&
|
|
isset($this->_oids[$oc][$attr])) {
|
|
return $this->_oids[$oc][$attr];
|
|
}
|
|
throw new Horde_Ldap_Exception("Could not find $attr attributes for $oc ");
|
|
}
|
|
|
|
/**
|
|
* Returns the name(s) of the immediate superclass(es).
|
|
*
|
|
* @param string $oc Name or OID of objectclass.
|
|
*
|
|
* @return array
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function superclass($oc)
|
|
{
|
|
$o = $this->get('objectclass', $oc);
|
|
return isset($o['sup']) ? $o['sup'] : array();
|
|
}
|
|
|
|
/**
|
|
* Parses the schema of the given subschema entry.
|
|
*
|
|
* @param Horde_Ldap_Entry $entry Subschema entry.
|
|
*/
|
|
public function parse($entry)
|
|
{
|
|
foreach ($this->types as $type => $attr) {
|
|
// Initialize map type to entry.
|
|
$type_var = '_' . $attr;
|
|
$this->{$type_var} = array();
|
|
|
|
if (!$entry->exists($attr)) {
|
|
continue;
|
|
}
|
|
|
|
// Get values for this type.
|
|
$values = $entry->getValue($attr);
|
|
if (!is_array($values)) {
|
|
continue;
|
|
}
|
|
foreach ($values as $value) {
|
|
// Get the schema entry.
|
|
$schema_entry = $this->_parse_entry($value);
|
|
// Set the type.
|
|
$schema_entry['type'] = $type;
|
|
// Save a ref in $_oids.
|
|
$this->_oids[$schema_entry['oid']] = $schema_entry;
|
|
// Save refs for all names in type map.
|
|
$names = $schema_entry['aliases'];
|
|
$names[] = $schema_entry['name'];
|
|
foreach ($names as $name) {
|
|
$this->{$type_var}[Horde_String::lower($name)] = $schema_entry;
|
|
}
|
|
}
|
|
}
|
|
$this->_initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Parses an attribute value into a schema entry.
|
|
*
|
|
* @param string $value Attribute value.
|
|
*
|
|
* @return array Schema entry array.
|
|
*/
|
|
protected function _parse_entry($value)
|
|
{
|
|
// Tokens that have no value associated.
|
|
$noValue = array('single-value',
|
|
'obsolete',
|
|
'collective',
|
|
'no-user-modification',
|
|
'abstract',
|
|
'structural',
|
|
'auxiliary');
|
|
|
|
// Tokens that can have multiple values.
|
|
$multiValue = array('must', 'may', 'sup');
|
|
|
|
// Get an array of tokens.
|
|
$tokens = $this->_tokenize($value);
|
|
|
|
// Remove surrounding brackets.
|
|
if ($tokens[0] == '(') {
|
|
array_shift($tokens);
|
|
}
|
|
if ($tokens[count($tokens) - 1] == ')') {
|
|
array_pop($tokens);
|
|
}
|
|
|
|
// First token is the oid.
|
|
$schema_entry = array('aliases' => array(),
|
|
'oid' => array_shift($tokens));
|
|
|
|
// Cycle over the tokens until none are left.
|
|
while (count($tokens) > 0) {
|
|
$token = Horde_String::lower(array_shift($tokens));
|
|
if (in_array($token, $noValue)) {
|
|
// Single value token.
|
|
$schema_entry[$token] = 1;
|
|
} else {
|
|
// Follow a string or a list if it is multivalued.
|
|
if (($schema_entry[$token] = array_shift($tokens)) == '(') {
|
|
// Create the list of values and cycles through the tokens
|
|
// until the end of the list is reached ')'.
|
|
$schema_entry[$token] = array();
|
|
while ($tmp = array_shift($tokens)) {
|
|
if ($tmp == ')') {
|
|
break;
|
|
}
|
|
if ($tmp != '$') {
|
|
$schema_entry[$token][] = $tmp;
|
|
}
|
|
}
|
|
}
|
|
// Create an array if the value should be multivalued but was
|
|
// not.
|
|
if (in_array($token, $multiValue) &&
|
|
!is_array($schema_entry[$token])) {
|
|
$schema_entry[$token] = array($schema_entry[$token]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get max length from syntax.
|
|
if (isset($schema_entry['syntax'])) {
|
|
if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
|
|
$schema_entry['max_length'] = $matches[1];
|
|
}
|
|
}
|
|
|
|
// Force a name.
|
|
if (empty($schema_entry['name'])) {
|
|
$schema_entry['name'] = $schema_entry['oid'];
|
|
}
|
|
|
|
// Make one name the default and put the other ones into aliases.
|
|
if (is_array($schema_entry['name'])) {
|
|
$aliases = $schema_entry['name'];
|
|
$schema_entry['name'] = array_shift($aliases);
|
|
$schema_entry['aliases'] = $aliases;
|
|
}
|
|
|
|
return $schema_entry;
|
|
}
|
|
|
|
/**
|
|
* Tokenizes the given value into an array of tokens.
|
|
*
|
|
* @param string $value String to parse.
|
|
*
|
|
* @return array Array of tokens.
|
|
*/
|
|
protected function _tokenize($value)
|
|
{
|
|
/* Match one big pattern where only one of the three subpatterns
|
|
* matches. We are interested in the subpatterns that matched. If it
|
|
* matched its value will be non-empty and so it is a token. Tokens may
|
|
* be round brackets, a string, or a string enclosed by ''. */
|
|
preg_match_all("/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x", $value, $matches);
|
|
|
|
$tokens = array();
|
|
// Number of tokens (full pattern match).
|
|
for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
|
|
// Each subpattern.
|
|
for ($j = 1; $j < 4; $j++) {
|
|
// Pattern match in this subpattern.
|
|
if (null != trim($matches[$j][$i])) {
|
|
// This is the token.
|
|
$tokens[$i] = trim($matches[$j][$i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $tokens;
|
|
}
|
|
|
|
/**
|
|
* Returns wether a attribute syntax is binary or not.
|
|
*
|
|
* This method is used by Horde_Ldap_Entry to decide which PHP function
|
|
* needs to be used to fetch the value in the proper format (e.g. binary or
|
|
* string).
|
|
*
|
|
* @param string $attribute The name of the attribute (eg.: 'sn').
|
|
*
|
|
* @return boolean True if the attribute is a binary type.
|
|
*/
|
|
public function isBinary($attribute)
|
|
{
|
|
// All syntax that should be treaten as containing binary values.
|
|
$syntax_binary = array(self::SYNTAX_OCTET_STRING, self::SYNTAX_JPEG);
|
|
|
|
// Check Syntax.
|
|
try {
|
|
$attr_s = $this->get('attribute', $attribute);
|
|
} catch (Horde_Ldap_Exception $e) {
|
|
// Attribute not found in schema, consider attr not binary.
|
|
return false;
|
|
}
|
|
|
|
if (isset($attr_s['syntax']) &&
|
|
in_array($attr_s['syntax'], $syntax_binary)) {
|
|
// Syntax is defined as binary in schema
|
|
return true;
|
|
}
|
|
|
|
// Syntax not defined as binary, or not found if attribute is a
|
|
// subtype, check superior attribute syntaxes.
|
|
if (isset($attr_s['sup'])) {
|
|
foreach ($attr_s['sup'] as $superattr) {
|
|
if ($this->isBinary($superattr)) {
|
|
// Stop checking parents since we are binary.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|