nur-sery/nur_src/ldap/LdapConn.php

425 lines
13 KiB
PHP
Raw Normal View History

2024-04-04 22:21:20 +04:00
<?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);
}
/** @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");
}
}
}