376 lines
13 KiB
PHP
376 lines
13 KiB
PHP
|
<?php
|
||
|
namespace nur\ldap;
|
||
|
|
||
|
use ArrayAccess;
|
||
|
use Countable;
|
||
|
use nur\A;
|
||
|
use nur\b\IllegalAccessException;
|
||
|
use nur\ldap\syntaxes\StringSyntax;
|
||
|
|
||
|
/**
|
||
|
* Class LdapObject: un objet LDAP
|
||
|
*/
|
||
|
class LdapObject implements ArrayAccess, Countable {
|
||
|
static function with(?string $dn, ?array $entry): ?self {
|
||
|
if ($entry === null) return null;
|
||
|
else return (new self())->load($dn, $entry);
|
||
|
}
|
||
|
|
||
|
/** @var string[] liste des classes par défaut lors de la création de l'objet */
|
||
|
const OBJECT_CLASSES = ["top"];
|
||
|
/** @var string DN dans lequel cet objet est créé par défaut */
|
||
|
const PARENT_RDN = null;
|
||
|
/**
|
||
|
* @var array|string nom des attribut(s) utilisé(s) pour nommer cet objet par
|
||
|
* défaut
|
||
|
*/
|
||
|
const DN_NAMES = null;
|
||
|
|
||
|
function __construct(?string $dn=null, ?array $attrs=null, ?array $initialNames=null, ?LdapConn $conn=null) {
|
||
|
$this->reset($dn, $attrs, A::with($initialNames), $conn);
|
||
|
}
|
||
|
|
||
|
/** @var LdapConn */
|
||
|
protected $conn;
|
||
|
|
||
|
function getConn(): LdapConn {
|
||
|
return $this->conn;
|
||
|
}
|
||
|
|
||
|
/** @var array attributs initialement demandés lors de la recherche */
|
||
|
protected $initialNames;
|
||
|
|
||
|
protected function initialNames(): array {
|
||
|
return $this->initialNames;
|
||
|
}
|
||
|
|
||
|
/** @var array valeurs originale des attributs avant modification */
|
||
|
protected $orig;
|
||
|
|
||
|
/** @var array */
|
||
|
protected $data;
|
||
|
|
||
|
/** @var array */
|
||
|
protected $lkey2names;
|
||
|
|
||
|
/** @var array liste des attributs utilisés pour nommer l'objet */
|
||
|
protected $dnNames;
|
||
|
|
||
|
/**
|
||
|
* @var LdapAttr[] pour chaque attribut, l'instance de {@link LdapAttr} qui
|
||
|
* gère les valeurs correspondantes de $data
|
||
|
*/
|
||
|
protected $attrs;
|
||
|
|
||
|
protected function resetAttrs(): void {
|
||
|
# refaire les attributs le cas échéant
|
||
|
if ($this->attrs === null) return;
|
||
|
foreach (array_keys($this->data) as $name) {
|
||
|
if (array_key_exists($name, $this->attrs)) {
|
||
|
$this->attrs[$name]->reset($this->data[$name]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function n($key): string {
|
||
|
$lkey = strtolower(strval($key));
|
||
|
$name = A::get($this->lkey2names, $lkey);
|
||
|
if ($name === null) {
|
||
|
# si $key n'existe pas, l'ajouter
|
||
|
$name = $this->lkey2names[$lkey] = $key;
|
||
|
}
|
||
|
return $name;
|
||
|
}
|
||
|
|
||
|
function &array(): ?array { return $this->data; }
|
||
|
function count(): int { return count($this->data); }
|
||
|
function keys(): array { return array_keys($this->data); }
|
||
|
function has($name): bool {
|
||
|
return $this->data !== null && array_key_exists($this->n($name), $this->data);
|
||
|
}
|
||
|
function _get(string $name): LdapAttr {
|
||
|
$name = $this->n($name);
|
||
|
if ($this->attrs === null || !array_key_exists($name, $this->attrs)) {
|
||
|
$attribute = A::get(static::SCHEMA(), strtolower($name));
|
||
|
if ($attribute !== null && $this->conn !== null) {
|
||
|
["class" => $class, "flags" => $flags] = $attribute;
|
||
|
$syntax = $this->conn->getSyntax($class);
|
||
|
} else {
|
||
|
$syntax = $flags = null;
|
||
|
}
|
||
|
if ($syntax !== null) {
|
||
|
$attr = $syntax->newAttr($name, $this->data[$name], $flags);
|
||
|
} else {
|
||
|
$attr = new LdapAttr($name, $this->data[$name], $syntax, $flags);
|
||
|
}
|
||
|
$this->attrs[$name] = $attr;
|
||
|
}
|
||
|
return $this->attrs[$name];
|
||
|
}
|
||
|
function _del(string $name): void {
|
||
|
unset($this->data[$this->n($name)]);
|
||
|
}
|
||
|
function get($name) { return $this->_get($name)->get(); }
|
||
|
function first($name) { return $this->_get($name)->first(); }
|
||
|
function all($name): iterable { return $this->_get($name)->all(); }
|
||
|
function set($name, $values, bool $unlessNn=false): self { $this->_get($name)->set($values, $unlessNn); return $this; }
|
||
|
function add($name, $value, bool $unique=true): self { $this->_get($name)->add($value, $unique); return $this; }
|
||
|
function del($name, $value, int $maxCount=-1, bool $strict=false): self { $this->_get($name)->del($value, $maxCount, $strict); return $this; }
|
||
|
function ins($name, int $index, $value): self { $this->_get($name)->ins($index, $value); return $this; }
|
||
|
function unset($name, int $index): self { $this->_get($name)->unset($index); return $this; }
|
||
|
function merge(?array $attrs): self {
|
||
|
if ($attrs !== null) {
|
||
|
foreach ($attrs as $name => $values) {
|
||
|
$this->set($name, $values);
|
||
|
}
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
function offsetExists($key) { return $this->has($key); }
|
||
|
function offsetGet($key) { return $this->_get($key)->get(); }
|
||
|
function offsetSet($key, $value) { $this->_get($key)->set($value); }
|
||
|
function offsetUnset($key) { $this->_del($key); }
|
||
|
|
||
|
function __isset($key) { return $this->has($key); }
|
||
|
function __get($key) { return $this->_get($key)->get(); }
|
||
|
function __set($key, $value) { $this->_get($key)->set($value); }
|
||
|
function __unset($key) { $this->_del($key); }
|
||
|
|
||
|
/**
|
||
|
* initialiser cet objet avec des données construites à la volée.
|
||
|
* - si $dn === null, c'est un nouvel objet
|
||
|
* - sinon c'est un objet existant déjà dans LDAP
|
||
|
*/
|
||
|
function reset(?string $dn, ?array $attrs=null, ?array $initialNames=null, ?LdapConn $conn=null): self {
|
||
|
if ($conn !== null) $this->conn = $conn;
|
||
|
if ($initialNames !== null) $this->initialNames = $initialNames;
|
||
|
# attributs demandés
|
||
|
$lkey2names = ["dn" => "dn"];
|
||
|
foreach ($this->initialNames() as $name) {
|
||
|
if ($name == "+" || $name == "*") continue;
|
||
|
$lkey2names[strtolower($name)] = $name;
|
||
|
}
|
||
|
# attributs obtenus effectivement
|
||
|
A::merge_nn($attrs, [
|
||
|
"objectClass" => static::OBJECT_CLASSES,
|
||
|
]);
|
||
|
$orig = ["dn" => [$dn]];
|
||
|
foreach ($attrs as $name => $value) {
|
||
|
$orig[$name] = $value;
|
||
|
$lkey2names[strtolower($name)] = $name;
|
||
|
}
|
||
|
# ensuite, mettre à null les attributs qui n'ont pas été obtenus
|
||
|
foreach ($lkey2names as $name) {
|
||
|
if (!array_key_exists($name, $orig)) {
|
||
|
$orig[$name] = null;
|
||
|
}
|
||
|
}
|
||
|
# calculer les clés qui composent le DN
|
||
|
$dnNames = names::get_dn_names($dn, $lkey2names);
|
||
|
# finaliser le paramétrage
|
||
|
$this->data = $this->orig = $orig;
|
||
|
$this->lkey2names = $lkey2names;
|
||
|
$this->dnNames = $dnNames;
|
||
|
$this->resetAttrs();
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/** initialiser cet objet avec le résultat d'une recherche */
|
||
|
function load(string $dn, array $entry): self {
|
||
|
[$this->orig, $this->lkey2names, $this->dnNames,
|
||
|
] = LdapSearch::cook($this->initialNames(), $dn, $entry);
|
||
|
$this->data = $this->orig;
|
||
|
$this->resetAttrs();
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/** recharger l'objet depuis le serveur */
|
||
|
function reload(?LdapConn $conn=null): self {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
$dn = $this->data["dn"][0];
|
||
|
$entry = $conn->_search($dn, [
|
||
|
"attrs" => $this->initialNames(),
|
||
|
"scope" => "base",
|
||
|
])->first($dn);
|
||
|
if ($entry === null) {
|
||
|
throw new IllegalAccessException("object $dn no longer exists");
|
||
|
}
|
||
|
return $this->load($dn, $entry);
|
||
|
}
|
||
|
|
||
|
function initDn(?string $parentDn=null, $dnNames=null, ?LdapConn $conn=null): void {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
if ($parentDn === null) $parentDn = static::PARENT_RDN;
|
||
|
if ($conn !== null) $parentDn = $conn->ensureDn($parentDn);
|
||
|
if ($dnNames === null) $dnNames = static::DN_NAMES;
|
||
|
$rdn = [];
|
||
|
foreach (A::with($dnNames) as $name) {
|
||
|
$rdn[$name] = $this->get($name);
|
||
|
}
|
||
|
$dn = names::join($rdn, $parentDn);
|
||
|
$this->data["dn"] = [$dn];
|
||
|
$this->dnNames = names::get_dn_names($dn, $this->lkey2names);
|
||
|
}
|
||
|
|
||
|
function computeAddattrs(array $data): array {
|
||
|
$attrs = [];
|
||
|
$first = true;
|
||
|
foreach ($data as $name => $values) {
|
||
|
if ($first) {
|
||
|
# ne pas inclure le DN
|
||
|
$first = false;
|
||
|
continue;
|
||
|
}
|
||
|
# ne pas inclure les valeurs vides et nulles
|
||
|
if ($values === null || $values === []) continue;
|
||
|
# utiliser array_values pour être sûr d'avoir un tableau séquentiel (les
|
||
|
# valeurs composites sont indexées sur la clé calculée)
|
||
|
$attrs[$name] = array_values(A::with($values));
|
||
|
}
|
||
|
return $attrs;
|
||
|
}
|
||
|
function computeModattr(string $name, $orig, $value): array {
|
||
|
# utiliser array_values pour être sûr d'avoir un tableau séquentiel (les
|
||
|
# valeurs composites sont indexées sur la clé calculée)
|
||
|
$orig = array_values(A::with($orig));
|
||
|
$value = array_values(A::with($value));
|
||
|
if ($value === $orig) return [];
|
||
|
if (!$orig) return [["add", $name => $value]];
|
||
|
elseif (!$value) return [["delete", $name]];
|
||
|
else return [["replace", $name => $value]];
|
||
|
#XXX pour certains attributs (comme member), ou si le nombre d'éléments
|
||
|
# dépasse un certain seuil, remplacer replace par un ensemble de add et/ou
|
||
|
# delete
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retourner true si update() provoquerait une mise à jour du serveur LDAP, en
|
||
|
* d'autres termes si l'objet est nouveau ou a des modifications
|
||
|
*/
|
||
|
function willUpdate(): bool {
|
||
|
$create = $this->orig["dn"][0] === null;
|
||
|
if ($create) return true;
|
||
|
foreach ($this->data as $name => $value) {
|
||
|
$orig = A::get($this->orig, $name);
|
||
|
$modattr = $this->computeModattr($name, $orig, $value);
|
||
|
if ($modattr != null) return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return bool true si la modification a été faite, false si elle n'était pas
|
||
|
* nécessaire
|
||
|
*/
|
||
|
function update($params=null, ?LdapConn $conn=null, ?bool $create=null): bool {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
$dn = $this->data["dn"][0];
|
||
|
if ($create === null) {
|
||
|
$origDn = $this->orig["dn"][0];
|
||
|
$create = $origDn === null;
|
||
|
}
|
||
|
if ($create) {
|
||
|
# création de l'objet
|
||
|
$attrs = $this->computeAddattrs($this->data);
|
||
|
$conn->add($dn, $attrs, $params);
|
||
|
} else {
|
||
|
# mise à jour de l'objet
|
||
|
$modattrs = [];
|
||
|
foreach ($this->data as $name => $value) {
|
||
|
$orig = A::get($this->orig, $name);
|
||
|
$modattr = $this->computeModattr($name, $orig, $value);
|
||
|
if ($modattr != null) {
|
||
|
if (in_array($name, $this->dnNames)) {
|
||
|
throw IllegalAccessException::not_allowed("modifying DN attrs");
|
||
|
}
|
||
|
A::merge($modattrs, $modattr);
|
||
|
}
|
||
|
}
|
||
|
if (!$modattrs) return false;
|
||
|
$conn->modify($dn, $modattrs);
|
||
|
}
|
||
|
# s'il y a des références sur $this->data, alors une simple "copie" fera
|
||
|
# que $this->orig garde ces références. c'est la raison pour laquelle on
|
||
|
# doit refaire les attributs
|
||
|
$this->orig = $this->data;
|
||
|
$this->attrs = null;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function rename(string $newRdn, $params=null, ?LdapConn $conn=null): void {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
$dn = $this->data["dn"][0];
|
||
|
if (ldap::prepare_rename($dn, $newRdn, $params)) {
|
||
|
$dn = $conn->rename($dn, $newRdn, $params);
|
||
|
$this->orig["dn"] = [$dn];
|
||
|
$this->data["dn"] = [$dn];
|
||
|
$this->dnNames = names::get_dn_names($dn, $this->lkey2names);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function delete($params=null, ?LdapConn $conn=null): void {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
$conn->delete($this->data["dn"][0], $params);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* tester s'il existe un objet nommé $attr=$value dans branche $parent qui
|
||
|
* vaut par défaut la branche dans laquelle est situé cet objet
|
||
|
*/
|
||
|
function existsSibling(string $value, ?string $attr=null, ?string $parent=null, ?LdapConn $conn=null): bool {
|
||
|
if ($conn === null) $conn = $this->conn;
|
||
|
$dn = $this->data["dn"][0];
|
||
|
names::split_dn($dn, $myRdn, $myParent);
|
||
|
if ($attr === null) {
|
||
|
$myAttrs = names::split_rdn($myRdn);
|
||
|
$attr = A::first_key($myAttrs);
|
||
|
}
|
||
|
if ($parent === null) $parent = $myParent;
|
||
|
$entry = $conn->_search(null, [
|
||
|
"scope" => "one",
|
||
|
"suffix" => $parent,
|
||
|
"filter" => [$attr => $value],
|
||
|
"attrs" => ["dn"],
|
||
|
])->first();
|
||
|
return $entry !== null;
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
static function _AUTOGEN_SCHEMA(): array {
|
||
|
return scheman::autogen_schema(static::OBJECT_CLASSES);
|
||
|
}
|
||
|
static function _AUTOGEN_PROPERTIES(): array {
|
||
|
return scheman::autogen_properties(self::_AUTOGEN_SCHEMA());
|
||
|
}
|
||
|
static function _AUTOGEN_METHODS(): array {
|
||
|
return scheman::autogen_methods(self::_AUTOGEN_SCHEMA());
|
||
|
}
|
||
|
const SCHEMA = null;
|
||
|
protected static function SCHEMA(): array {
|
||
|
# il faut au moins la définition qui indique que dn est monovalué
|
||
|
$schema = static::SCHEMA;
|
||
|
if ($schema === null) {
|
||
|
$schema = [
|
||
|
"dn" => [
|
||
|
"name" => "dn",
|
||
|
"class" => StringSyntax::class,
|
||
|
"flags" => LdapAttr::MONOVALUED,
|
||
|
],
|
||
|
];
|
||
|
}
|
||
|
return $schema;
|
||
|
}
|
||
|
function __call(string $name, ?array $args) {
|
||
|
$schema = static::SCHEMA();
|
||
|
if (is_array($schema) && array_key_exists(strtolower($name), $schema)) {
|
||
|
return $this->_get($name);
|
||
|
}
|
||
|
throw IllegalAccessException::not_implemented($name);
|
||
|
}
|
||
|
## rajouter ceci dans les classes dérivées
|
||
|
#const _AUTOGEN_CONSTS = ["SCHEMA"];
|
||
|
#const _AUTOGEN_PROPERTIES = [[self::class, "_AUTOGEN_PROPERTIES"]];
|
||
|
#const _AUTOGEN_METHODS = [[self::class, "_AUTOGEN_METHODS"]];
|
||
|
}
|