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