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