446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
namespace nur\ldap;
 | 
						|
 | 
						|
use nur\A;
 | 
						|
use nur\b\ICloseable;
 | 
						|
use nur\b\params\Parametrable;
 | 
						|
use nur\b\params\Tparametrable;
 | 
						|
use nur\ldap\schemas\LdapSchemaExtractor;
 | 
						|
use nur\ldap\schemas\SchemaManager;
 | 
						|
use nur\ldap\syntaxes\AbstractSyntax;
 | 
						|
use nur\path;
 | 
						|
use nur\php\SrcGenerator;
 | 
						|
use nur\str;
 | 
						|
use nur\writer;
 | 
						|
 | 
						|
/**
 | 
						|
 * Class LdapConn: une connexion à un serveur LDAP
 | 
						|
 */
 | 
						|
class LdapConn extends Parametrable implements ICloseable {
 | 
						|
  use Tparametrable;
 | 
						|
 | 
						|
  const URI = "ldap://localhost:389";
 | 
						|
  const BINDDN = null;
 | 
						|
  const PASSWORD = null;
 | 
						|
  const CONTROLS = null;
 | 
						|
 | 
						|
  const PARAMETRABLE_PARAMS_SCHEMA = [
 | 
						|
    "uri" => ["string", null, "URI du serveur LDAP"],
 | 
						|
    "binddn" => ["?string", null, "DN avec lequel se lier"],
 | 
						|
    "password" => ["?string", null, "mot de passe"],
 | 
						|
    "controls" => ["array", [], "contrôle de connexion"],
 | 
						|
    "protocol" => ["int", 3, "version du protocole"],
 | 
						|
    "autoconnect" => ["bool", true, "faut-il se connecter dès la création de l'objet?"],
 | 
						|
    # paramètres par défaut
 | 
						|
    "suffix" => ["?string", null, "DN de base du serveur"],
 | 
						|
    "domain" => ["?string", null, "domaine DNS de l'établissement"],
 | 
						|
    "etab" => ["?string", null, "code de l'établissement"],
 | 
						|
    "autofill_params" => ["bool", true, "faut-il calculer automatiquement les paramètres par défaut?"],
 | 
						|
    # configuration du serveur
 | 
						|
    "root_dse" => ["?array", null, "configuration du serveur"],
 | 
						|
    "ldap_syntaxes" => ["?array", null, "définition des syntaxes"],
 | 
						|
    "attribute_types" => ["?array", null, "définition des attributs"],
 | 
						|
    "object_classes" => ["?array", null, "définition des classes d'objets"],
 | 
						|
  ];
 | 
						|
 | 
						|
  function __construct(?array $params=null) {
 | 
						|
    self::set_parametrable_params_defaults($params, [
 | 
						|
      "uri" => static::URI,
 | 
						|
      "binddn" => static::BINDDN,
 | 
						|
      "password" => static::PASSWORD,
 | 
						|
      "controls" => static::CONTROLS,
 | 
						|
    ]);
 | 
						|
    parent::__construct($params);
 | 
						|
    if ($this->ppAutoconnect) $this->connect();
 | 
						|
    if ($this->ppAutofillParams) $this->fillParams();
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var string */
 | 
						|
  protected $ppUri;
 | 
						|
 | 
						|
  /** @var ?string */
 | 
						|
  protected $ppBinddn;
 | 
						|
 | 
						|
  /** @var ?string */
 | 
						|
  protected $ppPassword;
 | 
						|
 | 
						|
  /** @var ?array */
 | 
						|
  protected $ppControls;
 | 
						|
 | 
						|
  /** @var int */
 | 
						|
  protected $ppProtocol;
 | 
						|
 | 
						|
  /** @var bool */
 | 
						|
  protected $ppAutoconnect;
 | 
						|
 | 
						|
  /** @var ?string */
 | 
						|
  protected $ppSuffix;
 | 
						|
 | 
						|
  function getSuffix(): ?string {
 | 
						|
    return $this->ppSuffix;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var ?string */
 | 
						|
  protected $ppDomain;
 | 
						|
 | 
						|
  function getDomain(): ?string {
 | 
						|
    return $this->ppDomain;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var ?string */
 | 
						|
  protected $ppEtab;
 | 
						|
 | 
						|
  function getEtab(bool $withPrefix=true): ?string {
 | 
						|
    $etab = $this->ppEtab;
 | 
						|
    if (!$withPrefix) {
 | 
						|
      $etab = preg_replace('/^\{[^}]+}/', "", $etab);
 | 
						|
    }
 | 
						|
    return $etab;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var bool */
 | 
						|
  protected $ppAutofillParams;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param resource $conn
 | 
						|
   * @throws LdapException
 | 
						|
   */
 | 
						|
  function tryConnect(?string $binddn=null, ?string $password=null, ?array $controls=null, $conn=null) {
 | 
						|
    if ($conn === null) {
 | 
						|
      $uri = $this->ppUri;
 | 
						|
      $conn = LdapException::check("connect $uri", null
 | 
						|
        , ldap_connect($uri));
 | 
						|
      $procotol = $this->ppProtocol;
 | 
						|
      LdapException::check("set_option protocol=$procotol", $conn
 | 
						|
        , ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, $procotol));
 | 
						|
    }
 | 
						|
 | 
						|
    if ($binddn === null) $binddn = $this->ppBinddn;
 | 
						|
    if ($password === null) $password = $this->ppPassword;
 | 
						|
    if ($controls === null) $controls = $this->ppControls;
 | 
						|
    $operation = "bind $binddn";
 | 
						|
    $r = LdapException::check($operation, $conn
 | 
						|
      , ldap_bind_ext($conn, $binddn, $password, $controls));
 | 
						|
    LdapException::check_result($operation, $conn, $r);
 | 
						|
 | 
						|
    return $conn;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var resource */
 | 
						|
  protected $conn;
 | 
						|
 | 
						|
  function connect(?string $binddn=null, ?string $password=null, ?array $controls=null): void {
 | 
						|
    $this->conn = $this->tryConnect($binddn, $password, $controls, $this->conn);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * se reconnecter, mais seulement s'il y a une erreur sur la connection
 | 
						|
   *
 | 
						|
   * @return true si la reconnexion a effectivement eu lieu
 | 
						|
   */
 | 
						|
  function reconnect(bool $force=false): bool {
 | 
						|
    if (!$force) {
 | 
						|
      try {
 | 
						|
        $this->_search(null, [
 | 
						|
          "attrs" => ["objectClass"],
 | 
						|
          "scope" => "base",
 | 
						|
          "suffix" => "",
 | 
						|
        ])->first();
 | 
						|
      } catch (LdapException $e) {
 | 
						|
        $force = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if ($force) $this->connect();
 | 
						|
    return $force;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @return resource */
 | 
						|
  protected function conn() {
 | 
						|
    if ($this->conn === null) $this->connect();
 | 
						|
    return $this->conn;
 | 
						|
  }
 | 
						|
 | 
						|
  /** retourner un objet vide permettant de construire un objet depuis zéro */
 | 
						|
  function empty(?LdapObject $object=null): LdapObject {
 | 
						|
    if ($object === null) $object = new LdapObject();
 | 
						|
    return $object->reset(null, null, [], $this);
 | 
						|
  }
 | 
						|
 | 
						|
  function _search(?string $searchbase=null, $params=null): LdapSearch {
 | 
						|
    LdapSearch::search_md()->ensureSchema($params);
 | 
						|
    A::replace_n($params, "searchbase", $searchbase);
 | 
						|
    A::replace_n($params, "suffix", $this->ppSuffix);
 | 
						|
    return new LdapSearch($this->conn(), $params);
 | 
						|
  }
 | 
						|
 | 
						|
  function search(?string $searchbase=null, $params=null, ?ILdapWalker $walker=null): ILdapWalker {
 | 
						|
    if ($walker === null) {
 | 
						|
      $walker = new LdapWalker($this);
 | 
						|
    } else {
 | 
						|
      $walker->close();
 | 
						|
      $walker->reset(null, null, null, $this);
 | 
						|
    }
 | 
						|
    return $walker->resetSearch($this->_search($searchbase, $params));
 | 
						|
  }
 | 
						|
 | 
						|
  function first(?string $searchbase=null, $params=null, ?LdapObject $object=null): ?LdapObject {
 | 
						|
    $search = $this->_search($searchbase, $params);
 | 
						|
    $entry = $search->first($dn);
 | 
						|
    if ($entry === null) return null;
 | 
						|
    else return $this->empty($object)->load($dn, $entry);
 | 
						|
  }
 | 
						|
 | 
						|
  function read(string $dn, ?array $params=null, ?LdapObject $object=null): ?LdapObject {
 | 
						|
    A::merge($params, [
 | 
						|
      "scope" => "base",
 | 
						|
      "suffix" => $dn,
 | 
						|
    ]);
 | 
						|
    return $this->first(null, $params, $object);
 | 
						|
  }
 | 
						|
 | 
						|
  function add(string $dn, array $attrs, $params=null): void {
 | 
						|
    ldap::add($this->conn(), $dn, $attrs, $params);
 | 
						|
  }
 | 
						|
 | 
						|
  function modify(string $dn, array $modattrs, $params=null): void {
 | 
						|
    ldap::modify($this->conn(), $dn, $modattrs, $params);
 | 
						|
  }
 | 
						|
 | 
						|
  function rename(string $dn, string $newRdn, $params=null): string {
 | 
						|
    if (ldap::prepare_rename($dn, $newRdn, $params)) {
 | 
						|
      return ldap::rename($this->conn(), $dn, $newRdn, $params);
 | 
						|
    } else {
 | 
						|
      # renommage non nécessaire
 | 
						|
      return $dn;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function delete(string $dn, $params=null): void {
 | 
						|
    ldap::delete($this->conn(), $dn, $params);
 | 
						|
  }
 | 
						|
 | 
						|
  function close(): void {
 | 
						|
    if ($this->conn !== null) {
 | 
						|
      ldap_unbind($this->conn);
 | 
						|
      $this->conn = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #############################################################################
 | 
						|
 | 
						|
  /**
 | 
						|
   * Si $rdn se termine par le suffixe, le retourner tel quel, sinon rajouter
 | 
						|
   * le suffixe si ce n'est pas un DN qui est dans un des contextes valides
 | 
						|
   */
 | 
						|
  function ensureDn(string $rdn): string {
 | 
						|
    $suffix = $this->ppSuffix;
 | 
						|
    if (names::have_suffix($rdn, $suffix)) return $rdn;
 | 
						|
    $rootDse = $this->getRootDseForContexts();
 | 
						|
    $namingContexts = $rootDse->get("namingContexts", []);
 | 
						|
    foreach ($namingContexts as $namingContext) {
 | 
						|
      if (names::have_suffix($rdn, $suffix)) return $rdn;
 | 
						|
    }
 | 
						|
    return names::join($rdn, $suffix);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Corriger un label de la forme {UAI::XXX} en insérant le code de
 | 
						|
   * l'établissement
 | 
						|
   */
 | 
						|
  function fixLabel(string $labeledValue): string {
 | 
						|
    if (!preg_match('/^(\{[A-Za-z0-9:._-]+})(.*)/', $labeledValue, $ms)) {
 | 
						|
      return $labeledValue;
 | 
						|
    }
 | 
						|
    $label = $ms[1];
 | 
						|
    $value = $ms[2];
 | 
						|
    if (str::del_prefix($label, "{UAI::")) {
 | 
						|
      $label = "{UAI:".$this->getEtab(false).":$label";
 | 
						|
    } elseif (str::del_prefix($label, "{UAI:}")) {
 | 
						|
      $label = "{UAI:".$this->getEtab(false)."}$label";
 | 
						|
    }
 | 
						|
    return $label.$value;
 | 
						|
  }
 | 
						|
 | 
						|
  #############################################################################
 | 
						|
 | 
						|
  /** @var SchemaManager */
 | 
						|
  protected $scheman;
 | 
						|
 | 
						|
  protected function scheman(): SchemaManager {
 | 
						|
    if ($this->scheman === null) {
 | 
						|
      $this->scheman = new SchemaManager($this);
 | 
						|
    }
 | 
						|
    return $this->scheman;
 | 
						|
  }
 | 
						|
 | 
						|
  function getSyntax($class): AbstractSyntax {
 | 
						|
    $syntax = $this->scheman()->getSyntax($class);
 | 
						|
    $syntax->initConn($this);
 | 
						|
    return $syntax;
 | 
						|
  }
 | 
						|
 | 
						|
  #############################################################################
 | 
						|
 | 
						|
  protected function loadRootDse(?array $attrs=null): LdapObject {
 | 
						|
    if ($attrs === null) $attrs = ["+", "*"];
 | 
						|
    $entry = $this->_search(null, [
 | 
						|
      "attrs" => $attrs,
 | 
						|
      "scope" => "base",
 | 
						|
      "suffix" => "",
 | 
						|
    ])->first($dn);
 | 
						|
    return $this->empty()->load($dn, $entry);
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var LdapObject */
 | 
						|
  protected $ppRootDse;
 | 
						|
 | 
						|
  function pp_setRootDse(array $rootDse) {
 | 
						|
    $this->ppRootDse = $this->empty()->reset("", $rootDse);
 | 
						|
  }
 | 
						|
 | 
						|
  function getRootDse(): LdapObject {
 | 
						|
    if ($this->ppRootDse === null) $this->ppRootDse = $this->loadRootDse();
 | 
						|
    return $this->ppRootDse;
 | 
						|
  }
 | 
						|
 | 
						|
  protected function getRootDseForContexts(): LdapObject {
 | 
						|
    $rootDse = $this->ppRootDse;
 | 
						|
    if ($rootDse === null) {
 | 
						|
      $rootDse = $this->loadRootDse(["defaultNamingContext", "namingContexts"]);
 | 
						|
    }
 | 
						|
    return $rootDse;
 | 
						|
  }
 | 
						|
 | 
						|
  protected function loadTopObject(?array $attrs=null): LdapObject {
 | 
						|
    if ($attrs === null) $attrs = ["+", "*"];
 | 
						|
    $entry = $this->_search("", [
 | 
						|
      "attrs" => $attrs,
 | 
						|
      "scope" => "base",
 | 
						|
    ])->first($dn);
 | 
						|
    return $this->empty()->load($dn, $entry);
 | 
						|
  }
 | 
						|
 | 
						|
  protected $ppLdapSyntaxes;
 | 
						|
 | 
						|
  protected $ppAttributeTypes;
 | 
						|
 | 
						|
  protected $ppObjectClasses;
 | 
						|
 | 
						|
  function getSchemaInfos(): array {
 | 
						|
    $ldapSyntaxes = $this->ppLdapSyntaxes;
 | 
						|
    $attributeTypes = $this->ppAttributeTypes;
 | 
						|
    $objectClasses = $this->ppObjectClasses;
 | 
						|
    if ($ldapSyntaxes === null || $attributeTypes === null || $objectClasses === null) {
 | 
						|
      $lse = new LdapSchemaExtractor();
 | 
						|
      [
 | 
						|
        "ldap_syntaxes" => $ldapSyntaxes,
 | 
						|
        "attribute_types" => $attributeTypes,
 | 
						|
        "object_classes" => $objectClasses,
 | 
						|
      ] = $lse->loadSchema($this);
 | 
						|
    }
 | 
						|
    return [
 | 
						|
      "ldap_syntaxes" => $this->ppLdapSyntaxes = $ldapSyntaxes,
 | 
						|
      "attribute_types" => $this->ppAttributeTypes = $attributeTypes,
 | 
						|
      "object_classes" => $this->ppObjectClasses = $objectClasses,
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  function saveConfig($output, bool $overwriteShared=false): void {
 | 
						|
    $uri = $this->ppUri;
 | 
						|
    $sharedname = ldap_config::get_shared_file($uri);
 | 
						|
    if (is_string($output)) {
 | 
						|
      # corriger éventuellement le nom du fichier
 | 
						|
      $output = ldap_config::get_file($output);
 | 
						|
      # calculer le chemin vers fichier partagé
 | 
						|
      $shared = path::join(path::dirname($output), $sharedname);
 | 
						|
      # écrire la configuration partagée
 | 
						|
      if ($overwriteShared) {
 | 
						|
        # forcer le recalcul
 | 
						|
        $this->ppRootDse = null;
 | 
						|
        $this->ppLdapSyntaxes = null;
 | 
						|
        $this->ppAttributeTypes = null;
 | 
						|
        $this->ppObjectClasses = null;
 | 
						|
      }
 | 
						|
      if (!file_exists($shared) || $overwriteShared) {
 | 
						|
        $rootDse = $this->getRootDse()->array();
 | 
						|
        [
 | 
						|
          "ldap_syntaxes" => $ldapSyntaxes,
 | 
						|
          "attribute_types" => $attributeTypes,
 | 
						|
          "object_classes" => $objectClasses,
 | 
						|
        ] = $this->getSchemaInfos();
 | 
						|
        $config = [
 | 
						|
          "uri" => $uri,
 | 
						|
          "controls" => $this->ppControls,
 | 
						|
          "protocol" => $this->ppProtocol,
 | 
						|
          "suffix" => $this->ppSuffix,
 | 
						|
          "domain" => $this->ppDomain,
 | 
						|
          "etab" => $this->ppEtab,
 | 
						|
          "root_dse" => $rootDse,
 | 
						|
          "ldap_syntaxes" => $ldapSyntaxes,
 | 
						|
          "attribute_types" => $attributeTypes,
 | 
						|
          "object_classes" => $objectClasses,
 | 
						|
        ];
 | 
						|
        $src = new SrcGenerator();
 | 
						|
        $literals = [];
 | 
						|
        foreach (consts::LDAP_CONTROL_CONSTANTS as $constant) {
 | 
						|
          if (defined($constant)) {
 | 
						|
            $literals[] = [constant($constant), $constant];
 | 
						|
          }
 | 
						|
        }
 | 
						|
        A::merge($literals, consts::ROOT_DSE_LITERALS);
 | 
						|
        $src
 | 
						|
          ->genSof()
 | 
						|
          ->genLiteral("# shared configuration for $uri")
 | 
						|
          ->genReturn($config, null, $literals);
 | 
						|
        writer::with($shared, "wb")->writeLines($src->getLines())->close();
 | 
						|
      }
 | 
						|
    }
 | 
						|
    # écrire la configuration
 | 
						|
    $config = [
 | 
						|
      "binddn" => $this->ppBinddn,
 | 
						|
      "password" => $this->ppPassword,
 | 
						|
    ];
 | 
						|
    $src = new SrcGenerator();
 | 
						|
    $src
 | 
						|
      ->genSof()
 | 
						|
      ->genLiteral("return array_merge(require __DIR__.'/$sharedname',")
 | 
						|
      ->addValue($config)
 | 
						|
      ->genLiteral(");");
 | 
						|
    writer::with($output, "wb")->writeLines($src->getLines())->close();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * calculer automatiquement les paramètres par défaut s'ils ne sont pas
 | 
						|
   * spécifiés, tels que:
 | 
						|
   * - suffix
 | 
						|
   * - domain
 | 
						|
   * - etab
 | 
						|
   */
 | 
						|
  function fillParams(): void {
 | 
						|
    if ($this->ppSuffix === null) {
 | 
						|
      $rootDse = $this->getRootDseForContexts();
 | 
						|
      $suffix = $rootDse->get("defaultNamingContext");
 | 
						|
      if ($suffix === null) {
 | 
						|
        $namingContexts = $rootDse->get("namingContexts", []);
 | 
						|
        foreach ($namingContexts as $namingContext) {
 | 
						|
          if (str::_starts_with("dc=", strtolower($namingContext))) {
 | 
						|
            $suffix = $namingContext;
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if ($suffix === null) $suffix = $namingContexts[0];
 | 
						|
      }
 | 
						|
      $this->ppSuffix = $suffix;
 | 
						|
    }
 | 
						|
    if ($this->ppDomain === null) {
 | 
						|
      $parts = ldap_explode_dn($this->ppSuffix, 1);
 | 
						|
      unset($parts["count"]);
 | 
						|
      $this->ppDomain = implode(".", $parts);
 | 
						|
    }
 | 
						|
    if ($this->ppEtab === null) {
 | 
						|
      $topObject = $this->loadTopObject();
 | 
						|
      $this->ppEtab = $topObject->first("supannEtablissement");
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |