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