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"]]; }