nur-sery/nur_src/ldap/LdapObject.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"]];
}