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