Add schema hanlding methods & classes

This commit is contained in:
Benjamin Renard 2023-03-16 02:21:46 +01:00
parent 9520502ece
commit a5b19f39cc
10 changed files with 700 additions and 3 deletions

View file

@ -109,7 +109,7 @@ class Entry {
* (optional, default: null or array() if $all_values is True)
* @param bool $all_values If True, return all the attribute values, otherwise return only the
* first one (optional, default: true)
* @return mixed
* @return ( $all_values is True ? array<string> : mixed )
*/
public function get_values($attr, $default=null, $all_values=true) {
$attr = mb_strtolower($attr);

View file

@ -0,0 +1,5 @@
<?php
namespace EesyLDAP;
class InvalidPropertyException extends LdapException {}

View file

@ -20,6 +20,7 @@ namespace EesyLDAP;
* @property-read string $filter
* @property-read string $scope
* @property-read bool $raise_on_error
* @property-read bool $use_schema
*/
class Ldap {
@ -67,7 +68,8 @@ class Ldap {
'scope' => 'sub',
// Raise LdapException on error
'raise_on_error' => true,
// Use schema
'use_schema' => false,
);
/**
@ -89,6 +91,12 @@ class Ldap {
*/
protected $_link = false;
/**
* The Schema object
* @var Schema|false
*/
protected $schema = false;
/**
* Scopes associated with their search function
* @var array<string,callable>
@ -382,6 +390,13 @@ class Ldap {
return self :: $config_aliases;
case 'default_config':
return self :: $default_config;
case 'schema':
if ($this->schema)
return $this->schema;
if (!$this->use_schema)
return false;
$this->schema = Schema :: load($this);
return $this->schema;
}
return $this -> error("Invalid property '$key' requested");
}
@ -437,7 +452,7 @@ class Ldap {
* @param string|null $filter Filter of the search (optional, default: null == use config)
* @param array<string,mixed>|null $params Other optional parameters of the search (optional, default: null == use config)
* @param bool|null $raise See error() (optional, default: null)
* @return array<string|Entry>|false Array of Entry object with DN as key, or False in case of error
* @return array<string,Entry>|false Array of Entry object with DN as key, or False in case of error
* @throws LdapException
*/
public function search($base=null, $filter=null, $params=null, $raise=null) {
@ -493,9 +508,33 @@ class Ldap {
// @phpstan-ignore-next-line
$entries[$dn] = new Entry($dn, $attrs);
}
// @phpstan-ignore-next-line
return $entries;
}
/**
* Get one entry from LDAP directory by DN
*
* This method make easier to retreive a specific entry by its DN. A search will be runned on the
* specified DN with a base scope and if one and only one entry is found, it will be returned.
*
* @param string $dn DN of the expected entry
* @param string|null $filter Filter of the search (optional, default: null == use config)
* @param array<string,mixed>|null $params Other optional parameters of the search (optional,
* default: null == use config, see search())
* @param bool|null $raise See error() (optional, default: null)
* @return Entry|false The requested Entry object, or False in case of error
* @throws LdapException
*/
public function get_entry($dn, $filter=null, $params=null, $raise=null) {
$params = is_array($params)?$params:array();
$params['scope'] = 'base';
$entries = $this -> search($dn, $filter, $params, $raise);
if (is_array($entries) && count($entries) == 1)
return array_pop($entries);
return false;
}
/**
* Get parameter value
* @param array<string,mixed>|null $params Array of provided parameter values

194
src/Schema.php Normal file
View file

@ -0,0 +1,194 @@
<?php
namespace EesyLDAP;
class Schema {
/**
* CN=SubSchema Entry object
* @var Entry
*/
protected $entry;
/**
* LDAP connection
* @var Ldap
*/
protected $ldap;
/**
* Some syntax OID constants
*/
public const SYNTAX_BOOLEAN = '1.3.6.1.4.1.1466.115.121.1.7';
public const SYNTAX_DIRECTORY_STRING = '1.3.6.1.4.1.1466.115.121.1.15';
public const SYNTAX_DISTINGUISHED_NAME = '1.3.6.1.4.1.1466.115.121.1.12';
public const SYNTAX_INTEGER = '1.3.6.1.4.1.1466.115.121.1.27';
public const SYNTAX_JPEG = '1.3.6.1.4.1.1466.115.121.1.28';
public const SYNTAX_NUMERIC_STRING = '1.3.6.1.4.1.1466.115.121.1.36';
public const SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.38';
public const SYNTAX_OCTET_STRING = '1.3.6.1.4.1.1466.115.121.1.40';
/**
* Map SubSchema entry attributes to schema entry type
*
* @var array<string,string>
*/
protected static $attributes_to_entry_types = array(
'attributeTypes' => 'Attribute',
'matchingRules' => 'MatchingRule',
'matchingRuleUse' => 'MatchingRuleUse',
'objectClasses' => 'ObjectClass',
'ldapSyntaxes' => 'Syntax',
);
/**
* Loaded schema entries ordered by type
* @var array<string,array<string,Schema\SchemaEntry>>
*/
protected $entries = array();
/**
* Loaded schema entries mapped with their OID
* @var array<string,Schema\SchemaEntry>
*/
protected $oids = array();
/**
* Constructor
* @param Ldap $ldap The LDAP connection
* @param Entry $entry The cn=SubSchema LDAP entry object
* @return void
*/
public function __construct(&$ldap, &$entry) {
$this -> ldap = $ldap;
$this -> entry = $entry;
$this -> parse();
}
/**
* Load schema from provided LDAP connection
* @param Ldap $ldap
* @param bool|null $raise
* @return Schema|false Schema object on success, false on error
*/
public static function load(&$ldap, $raise=null) {
$entry = $ldap->get_entry(
'cn=SubSchema', '(objectclass=*)',
array('attributes' => array_keys(self :: $attributes_to_entry_types))
);
if (!$entry instanceof Entry)
return $ldap->error('Fail to load cn=SubSchema entry', $raise);
return new Schema($ldap, $entry);
}
/**
* Parse SubSchema entry attributes values
* @return void
*/
protected function parse() {
foreach (self :: $attributes_to_entry_types as $attr => $type) {
$type_name = strtolower($type);
foreach($this -> entry -> get_values($attr, array(), true) as $value) {
// @phpstan-ignore-next-line
$entry = call_user_func("\\EesyLDAP\\Schema\\$type::parse", $value);
if (!$entry instanceof Schema\SchemaEntry) {
$this -> ldap -> error("Fail to parse %s schema value: %s", null, $attr, $value);
continue;
}
$oid = $entry->oid;
if ($type != 'MatchingRuleUse') {
if (array_key_exists($oid, $this->oids)) {
$this -> ldap -> error(
"Duplicate OID %s found in schema: %s / %s", null, $oid, $this->oids[$oid], $entry);
continue;
}
$this->oids[$oid] = $entry;
}
if (!array_key_exists($type_name, $this -> entries))
$this -> entries[$type_name] = array();
$name = $entry->name;
if (array_key_exists($name, $this -> entries[$type_name])) {
$this -> ldap -> error(
"Duplicate %s schema entry %s found: %s / %s",
null, $type, $name, $this -> entries[$type_name][$name], $entry
);
continue;
}
$this -> entries[$type_name][$name] = $entry;
}
}
}
/**
* Get a schema entry by type and name/oid
* @param string $type
* @param string $name_or_oid
* @return Schema\SchemaEntry|false
*/
protected function _get_entry($type, $name_or_oid) {
if (
array_key_exists($name_or_oid, $this -> oids)
&& is_a($this -> oids[$name_or_oid], "\\EesyLDAP\\Schema\\$type")
)
return $this -> oids[$name_or_oid];
$type_name = strtolower($type);
if (!array_key_exists($type_name, $this -> entries))
return false;
foreach($this -> entries[$type_name] as $attr)
if ($attr->is_me($name_or_oid))
return $attr;
return false;
}
/**
* Get an attribute by name or oid
* @param string $name_or_oid
* @return Schema\Attribute|false
*/
public function attribute($name_or_oid) {
// @phpstan-ignore-next-line
return $this -> _get_entry('Attribute', $name_or_oid);
}
/**
* Get an objectclass by name or oid
* @param string $name_or_oid
* @return \EesyLDAP\Schema\ObjectClass|false
*/
public function objectclass($name_or_oid) {
// @phpstan-ignore-next-line
return $this -> _get_entry('ObjectClass', $name_or_oid);
}
/**
* Get a matching rule by name or oid
* @param string $name_or_oid
* @return Schema\MatchingRule|false
*/
public function matching_rule($name_or_oid) {
// @phpstan-ignore-next-line
return $this -> _get_entry('MatchingRule', $name_or_oid);
}
/**
* Get a matching rule use by name or oid
* @param string $name_or_oid
* @return Schema\MatchingRuleUse|false
*/
public function matching_rule_use($name_or_oid) {
// @phpstan-ignore-next-line
return $this -> _get_entry('MatchingRuleUse', $name_or_oid);
}
/**
* Get a syntax by name or oid
* @param string $name_or_oid
* @return Schema\Syntax|false
*/
public function syntax($name_or_oid) {
// @phpstan-ignore-next-line
return $this -> _get_entry('Syntax', $name_or_oid);
}
}

85
src/Schema/Attribute.php Normal file
View file

@ -0,0 +1,85 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string $oid
* @property-read string $name
* @property-read string|null $desc
* @property-read string|null $description
* @property-read bool $obselete
* @property-read string|null $sup
* @property-read string|null $superior
* @property-read string|null $equality
* @property-read string|null $ordering
* @property-read string|null $substr
* @property-read string|null $substring
* @property-read string|null $syntax
* @property-read int|null $max_length
* @property-read bool $single
* @property-read bool $single_value
* @property-read bool $multiple
* @property-read bool $collective
* @property-read string|null $usage
* @property-read bool $no_user_modification
*/
class Attribute extends SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'name' => null,
'desc' => null,
'obsolete' => false,
'sup' => null,
'equality' => null,
'ordering' => null,
'substr' => null,
'syntax' => null,
'max_length' => null,
'single-value' => false,
'collective' => false,
'usage' => null,
'no-user-modification' => false,
);
/**
* Properties name aliases
* @var array<string,string>
*/
protected static $property_aliases = array(
'description' => 'desc',
'superior' => 'sup',
'substring' => 'substr',
'single' => 'single-value',
'single_value' => 'single-value',
'no_user_modification' => 'no-user-modification',
);
/**
* Computed properties name
* @var array<string>
*/
protected static $computed_properties = array(
'names',
'multiple',
);
/**
* Magic method to get attribute schema entry key
* @param string $key
* @return mixed
* @throws \EesyLDAP\InvalidPropertyException
*/
public function __get($key) {
switch($key) {
case 'multiple':
return !$this->single;
}
return parent::__get($key);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string $oid
* @property-read string $name
* @property-read string|null $syntax
*/
class MatchingRule extends SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'name' => null,
'syntax' => null,
);
}

View file

@ -0,0 +1,23 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string $oid
* @property-read string $name
* @property-read array<string> $applies
*/
class MatchingRuleUse extends SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'name' => null,
'applies' => array(),
);
}

View file

@ -0,0 +1,77 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string $oid
* @property-read string $name
* @property-read string|null $desc
* @property-read string|null $description
* @property-read bool $obselete
* @property-read string|null $sup
* @property-read string|null $superior
* @property-read bool $abstract
* @property-read bool $structural
* @property-read bool $auxiliary
* @property-read string|null $type
* @property-read array<string> $must
* @property-read array<string> $may
*/
class ObjectClass extends SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'name' => null,
'desc' => null,
'obselete' => false,
'sup' => null,
'abstract' => false,
'structural' => false,
'auxiliary' => false,
'must' => array(),
'may' => array(),
);
/**
* Properties name aliases
* @var array<string,string>
*/
protected static $property_aliases = array(
'description' => 'desc',
'superior' => 'sup',
);
/**
* Computed properties name
* @var array<string>
*/
protected static $computed_properties = array(
'names',
'type',
);
/**
* Magic method to get objectclass schema entry key
* @param string $key
* @return mixed
* @throws \EesyLDAP\InvalidPropertyException
*/
public function __get($key) {
switch($key) {
case 'type':
if ($this->abstract)
return 'abstract';
if ($this->structural)
return 'structural';
if ($this->auxiliary)
return 'auxiliary';
return null;
}
return parent::__get($key);
}
}

221
src/Schema/SchemaEntry.php Normal file
View file

@ -0,0 +1,221 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string $oid
* @property-read string $name
* @property-read array<string> $names
*/
class SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'name' => array(),
);
/**
* Properties name aliases
* @var array<string,string>
*/
protected static $property_aliases = array();
/**
* Computed properties name
* @var array<string>
*/
protected static $computed_properties = array(
'names',
);
/**
* Schema entry data parsed from SubSchema attribute value
* @see parse()
* @var array<string,mixed>
*/
protected $data;
/**
* Constructor
* @param array<string,mixed> $data Parsed schema entry data
* @return void
*/
public function __construct($data) {
$this -> data = $data;
}
/**
* Parses an SubSchema attribute value into a SchemaEntry object
*
* Note: mainly from PEAR Net_LDAP2_Schema::_parse_entry()
* @link https://pear.php.net/package/Net_LDAP2
*
* @param string $value Attribute value
*
* @access protected
* @return SchemaEntry
*/
public static function parse($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('name', 'must', 'may', 'sup');
// initilization
$schema_entry = array();
$tokens = self :: _tokenize($value); // get an array of tokens
// remove surrounding brackets
if ($tokens[0] == '(') array_shift($tokens);
if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
$schema_entry['oid'] = array_shift($tokens); // first token is the oid
// cycle over the tokens until none are left
while (count($tokens) > 0) {
$token = strtolower(array_shift($tokens));
if (in_array($token, $noValue)) {
$schema_entry[$token] = 1; // single value token
}
else {
// this one follows a string or a list if it is multivalued
if (($schema_entry[$token] = array_shift($tokens)) == '(') {
// this creates 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 != '$') array_push($schema_entry[$token], $tmp);
}
}
// create a 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 (key_exists('syntax', $schema_entry)) {
// @phpstan-ignore-next-line
if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
$schema_entry['max_length'] = intval($matches[1]);
}
}
$type = get_called_class();
return new $type($schema_entry);
}
/**
* Tokenizes the given value into an array of tokens
*
* Note: mainly from PEAR Net_LDAP2_Schema::_tokenize()
* @link https://pear.php.net/package/Net_LDAP2
*
* @param string $value String to parse
*
* @access protected
* @return array<string> Array of tokens
*/
protected static function _tokenize($value) {
$tokens = array(); // array of tokens
$matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns
// this one is taken from perl-ldap, modified for php
$pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
/**
* This one matches one big pattern wherin only one of the three subpatterns matched
* 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($pattern, $value, $matches);
for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match)
for ($j = 1; $j < 4; $j++) { // each subpattern
if (null != trim($matches[$j][$i])) { // pattern match in this subpattern
$tokens[$i] = trim($matches[$j][$i]); // this is the token
}
}
}
return $tokens;
}
/**
* Magic method to get schema entry key
* @param string $key
* @return mixed
* @throws \EesyLDAP\InvalidPropertyException
*/
public function __get($key) {
if (array_key_exists($key, static :: $property_aliases))
$key = static :: $property_aliases[$key];
if (
!array_key_exists($key, static :: $default_properties)
&& !!array_key_exists($key, static :: $computed_properties)
)
throw new \EesyLDAP\InvalidPropertyException(
"Invalid property '$key' requested on '".get_called_class()."'"
);
switch($key) {
case 'name':
return (
isset($this -> data['name']) && $this -> data['name']?
$this -> data['name'][0]:$this -> oid // @phpstan-ignore-line
);
case 'names':
return (
isset($this -> data['name']) && $this -> data['name']?
$this -> data['name']:array($this -> oid)
);
}
$default = static :: $default_properties[$key];
if (!array_key_exists($key, $this -> data))
return $default;
if (is_bool($default))
return boolval($this->data[$key]);
if (is_array($default) && !is_array($default))
return is_null($this->data[$key])?array():array($this->data[$key]);
return $this->data[$key];
}
/**
* Magic method to compute string representation of this schema entry object
* @return string
*/
public function __toString() {
return sprintf('%s<%s>', get_called_class(), $this->name);
}
/**
* Helper to check if the provided identifier match with this schema entry
* @param string $id
* @return bool
*/
public function is_me($id) {
if (!is_string($id))
return false;
if ($id == $this->oid)
return true;
$id = strtolower($id);
foreach($this->names as $name)
if (strtolower($name) == $id)
return true;
return false;
}
}

30
src/Schema/Syntax.php Normal file
View file

@ -0,0 +1,30 @@
<?php
namespace EesyLDAP\Schema;
/**
* @property-read string|null $oid
* @property-read string|null $desc
* @property-read string|null $description
*/
class Syntax extends SchemaEntry {
/**
* Default properties value
* @var array<string,mixed>
*/
protected static $default_properties = array(
'oid' => null,
'desc' => null,
);
/**
* Properties name aliases
* @var array<string,string>
*/
protected static $property_aliases = array(
'description' => 'desc',
);
}