début migration ldap

This commit is contained in:
Jephté Clain 2025-11-06 11:21:37 +04:00
parent 6219c47452
commit 65fbe881f4
47 changed files with 3732 additions and 0 deletions

25
cli/LdapApplication.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace cli;
use nulib\app\cli\Application;
abstract class LdapApplication extends Application {
use TLdapApplication;
const LOAD_PARAMS = true;
const LDAP_SECTION = [
"title" => "CONNEXION LDAP",
["-C", "--config", "args" => "file"],
["-H", "--uri", "args" => 1],
["-D", "--binddn", "args" => 1],
["-w", "--password", "args" => 1],
];
const ARGS = [
"sections" => [
self::VERBOSITY_SECTION,
self::LDAP_SECTION,
],
];
}

35
cli/LdapDeleteApp.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace cli;
use cli\LdapApplication;
use nulib\output\msg;
use nur\ldap\LdapSearch;
use nur\ldap\LdapWalker;
class LdapDeleteApp extends LdapApplication {
const ARGS = [
"merge" => parent::ARGS,
["-s", "--scope", "args" => 1],
["-b", "--searchbase", "args" => 1],
["-B", "--searchbase-exact", "args" => 1],
];
protected $scope;
protected $searchbase, $searchbaseExact;
function main() {
$conn = $this->getConn();
$params = [];
LdapSearch::parse_args($params, $this->args
, $this->searchbase, $this->searchbaseExact
, $this->scope);
/** @var LdapWalker $lo */
$lo = $conn->search(null, $params);
while ($lo->next($first)) {
msg::action("Suppression $lo[dn]");
$lo->delete();
msg::asuccess();
}
}
}

29
cli/LdapGetInfosApp.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace cli;
use cli\LdapApplication;
class LdapGetInfosApp extends LdapApplication {
const ARGS = [
"merge" => parent::ARGS,
["-o", "--output", "args" => 1],
["-f", "--overwrite-shared", "value" => true],
["-u", "--update", "value" => true, "help" => "Mettre à jour le fichier de connexion (nécessite --config et implique --output et --overwrite-shared)"]
];
protected $output, $overwriteShared = false;
protected $update = false;
function main() {
$conn = $this->getConn();
if ($this->update) {
$config = $this->config;
if ($config === null) {
self::die("Vous devez spécifier la configuration à mettre à jour");
}
$this->output = $config;
$this->overwriteShared = true;
}
$conn->saveConfig($this->output, $this->overwriteShared);
}
}

62
cli/LdapSearchApp.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace cli;
use cli\LdapApplication;
use nur\b\IllegalAccessException;
use nur\ldap\io\LdapWriter;
use nur\ldap\io\LdifWriter;
use nur\ldap\io\YamlWriter;
use nur\ldap\LdapSearch;
use nur\ldap\LdapWalker;
class LdapSearchApp extends LdapApplication {
const ARGS = [
"merge" => parent::ARGS,
["-s", "--scope", "args" => 1],
["-b", "--searchbase", "args" => 1],
["-B", "--searchbase-exact", "args" => 1],
["-o", "--output", "args" => "file"],
["group",
["-F", "--format", "args" => 1],
["--ldif", "dest" => "format", "value" => "ldif"],
["--yaml", "dest" => "format", "value" => "yaml"],
],
];
protected $scope;
protected $searchbase, $searchbaseExact;
protected $output;
protected $format = "ldif";
function getWriter(): LdapWriter {
switch ($this->format) {
case "ldif":
case "l":
return new LdifWriter($this->output);
case "yaml":
case "y":
return new YamlWriter($this->output);
}
throw IllegalAccessException::unexpected_state();
}
function main() {
$conn = $this->getConn();
$params = [];
LdapSearch::parse_args($params, $this->args
, $this->searchbase, $this->searchbaseExact
, $this->scope);
/** @var LdapWalker $lo */
$lo = $conn->search(null, $params);
$writer = null;
while ($lo->next($first)) {
if ($first) {
$first = false;
$writer = $this->getWriter();
}
$writer->write($lo);
}
if ($writer !== null) $writer->close();
}
}

34
cli/TLdapApplication.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace cli;
use nur\A;
use nur\ldap\LdapConn;
trait TLdapApplication {
protected $config;
protected $uri, $binddn, $password;
protected function fixConfig(?string &$config): void {
}
function getConn(?array $supplParams=null): LdapConn {
$config = $this->config;
$this->fixConfig($config);
$loadParams = static::LOAD_PARAMS;
$autoconnect = $autofillParams = null;
if ($config === null) {
$params = [];
} else {
$params = require $config;
if (!$loadParams) $autoconnect = $autofillParams = false;
}
A::merge($params, A::filter_n([
"uri" => $this->uri,
"binddn" => $this->binddn,
"password" => $this->password,
"autoconnect" => $autoconnect,
"autofill_params" => $autofillParams,
]), $supplParams);
return new LdapConn($params);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace nulib\ldap;
use nulib\ldap\syntaxes\CompositeSyntax;
/**
* Class CompositeAttr: une liste de valeurs composites
*/
class CompositeAttr extends LdapAttr {
function reset(?array &$values): self {
if ($values !== null) {
/** @var CompositeSyntax $syntax */
$syntax = $this->syntax;
$tmp = [];
foreach ($values as $value) {
$cvalue = $syntax->ldap2php($value);
$key = $cvalue->getKey();
$value = $cvalue->formatLdap();
$tmp[$key] = $value;
}
$values = $tmp;
}
$this->data =& $values;
return $this;
}
function add($value, bool $unique=true, bool $strict=false): self {
/** @var CompositeSyntax $syntax */
$syntax = $this->syntax;
$value = A::first($syntax->ensureArray($value));
$cvalue = $syntax->ensureComposite($value);
if ($cvalue !== null) {
$key = $cvalue->getKey();
$value = $cvalue->formatLdap();
$this->data[$key] = $value;
}
return $this;
}
function del($value, int $maxCount=-1, bool $strict=false): self {
if ($value !== null && $this->data !== null) {
/** @var CompositeSyntax $syntax */
$syntax = $this->syntax;
$value = A::first($syntax->ensureArray($value));
$cvalue = $syntax->ensureComposite($value);
if ($cvalue !== null) {
$key = $cvalue->getKey();
unset($this->data[$key]);
}
}
return $this;
}
function ins(int $index, $value): self {
throw IllegalAccessException::not_allowed("composite attrs don't use indexes");
}
function unset(int $index): self {
throw IllegalAccessException::not_allowed("composite attrs don't use indexes");
}
}

153
src/ldap/CompositeValue.php Normal file
View File

@ -0,0 +1,153 @@
<?php
namespace nulib\ldap;
/**
* Class CompositeValue: une valeur composite
*/
abstract class CompositeValue extends BaseArray {
/** @var array schéma des champs de la valeur composite */
const SCHEMA = null;
/** @var array syntaxes associées aux champs */
const SYNTAXES = null;
/** @var array liste et ordre des éléments obligatoires */
const MANDATORY_KEYS = null;
/** @var array liste et ordre des éléments facultatifs connus */
const OPTIONAL_KEYS = null;
/** @var array liste des clés qui identifient cet objet */
const KEY_KEYS = null;
static function compute_keys(array $values): string {
$keys = static::KEY_KEYS;
if ($keys === null) $keys = static::MANDATORY_KEYS;
if ($keys === null) $keys = array_keys($values);
$parts = [];
foreach ($keys as $key) {
$parts[] = A::get($values, $key);
}
return implode("-", $parts);
}
protected $ldapKeys, $keys, $optionalKeys;
protected $syntaxes;
/** initialiser l'objet */
function setup(LdapConn $conn): self {
$ldapKeys = [];
$keys = [];
$mandatoryKeys = ValueException::check_nn(static::MANDATORY_KEYS
, "Vous devez définir MANDATORY_KEYS");
$index = 0;
foreach ($mandatoryKeys as $key => $ldapKey) {
if ($key === $index) {
$index++;
$key = $ldapKey;
}
$ldapKeys[$key] = $ldapKey;
$keys[$ldapKey] = $key;
}
$optionalKeys = [];
$index = 0;
foreach (A::with(static::OPTIONAL_KEYS) as $key => $ldapKey) {
if ($key === $index) {
$index++;
$key = $ldapKey;
}
$ldapKeys[$key] = $ldapKey;
$keys[$ldapKey] = $key;
$optionalKeys[] = $key;
}
$schemaKeys = A::keys(static::SCHEMA);
foreach ($schemaKeys as $key) {
if (!in_array($key, $keys)) {
$ldapKeys[$key] = $key;
$keys[$key] = $key;
$optionalKeys[] = $key;
}
}
$this->ldapKeys = $ldapKeys;
$this->keys = $keys;
$this->optionalKeys = $optionalKeys;
##
$syntaxClasses = static::SYNTAXES;
if ($syntaxClasses !== null) {
$syntaxes = [];
foreach ($schemaKeys as $key) {
$class = A::get($syntaxClasses, $key);
if ($class !== null) {
$syntaxes[$key] = $conn->getSyntax($class);
}
}
$this->syntaxes = $syntaxes;
}
##
return $this;
}
function has($key): bool { return $this->_has($key); }
function &get($key, $default=null) { return $this->_get($key, $default); }
function set($key, $value): self { return $this->_set($key, $value); }
function add($value): self { return $this->_set(null, $value); }
function del($key): self { return $this->_del($key); }
/** obtenir la clé qui identifie cet objet */
function getKey(): string {
return self::compute_keys($this->data);
}
/** initialiser cet objet avec une valeur LDAP */
function parseLdap(string $value): self {
if (!preg_match_all('/\[.*?]/', $value, $ms)) {
throw ValueException::invalid_value($value, "composite value");
}
$this->data = [];
foreach ($ms[0] as $nameValue) {
if (preg_match('/\[(.*?)=(.*)]/', $nameValue, $ms)) {
$ldapKey = names::ldap_unescape($ms[1]);
$key = A::get($this->keys, $ldapKey, $ldapKey);
$value = names::ldap_unescape($ms[2]);
/** @var AbstractSyntax $syntax */
$syntax = A::get($this->syntaxes, $key);
if ($syntax !== null) $value = $syntax->ldap2php($value);
$this->data[$key] = $value;
}
}
return $this;
}
/** retourner cette valeur au format LDAP */
function formatLdap(): string {
$optionalKeys = $this->optionalKeys;
$parts = [];
foreach ($this->ldapKeys as $key => $ldapKey) {
$value = A::get($this->data, $key);
if ($value === null && in_array($key, $optionalKeys)) continue;
/** @var AbstractSyntax $syntax */
$syntax = A::get($this->syntaxes, $key);
if ($syntax !== null) $value = $syntax->php2ldap($value);
$ldapKey = ldap_escape($ldapKey, 0, LDAP_ESCAPE_FILTER);
$value = ldap_escape($value, 0, LDAP_ESCAPE_FILTER);
$parts[] = "[$ldapKey=$value]";
}
return implode("", $parts);
}
function reset(?array $values): CompositeValue {
$md = Metadata::with(static::SCHEMA);
$md->ensureSchema($values);
$this->data = $values;
return $this;
}
#############################################################################
static function _AUTOGEN_PROPERTIES(): array {
return cvalues::autogen_properties(static::SCHEMA);
}
## rajouter ceci dans les classes dérivées
#const _AUTOGEN_PROPERTIES = [[self::class, "_AUTOGEN_PROPERTIES"]];
}

9
src/ldap/ILdapWalker.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace nulib\ldap;
interface ILdapWalker extends ICloseable {
function resetSearch(LdapSearch $search): ILdapWalker;
function next(): bool;
}

View File

@ -0,0 +1,33 @@
<?php
namespace nulib\ldap;
/**
* Interface IObjectWorkflow: un objet permettant de créer et/ou mettre à jour
* un objet LDAP dans le cadre d'une synchronisation
*/
interface IObjectWorkflow {
/** retourner le nom du workflox */
function getWorkflowName(): string;
/**
* synchroniser les données spécifiées vers l'objet correspndant, en le créant
* si nécessaire.
*
* $updated=true si l'objet a été créé ou mis à jour, false sinon
*/
function createOrUpdate(array $data, ?array $params=null, ?bool &$updated=null): ?LdapObject;
/**
* modifier uniquement le mot de passe de l'objet correspondant
*
* @return bool true si l'objet correspondant a été trouvé et qu'il a été mis
* à jour
*/
function updatePassword(array $data, string $password): bool;
/**
* supprimer l'objet correspondant. retourner true si l'objet a été supprimé,
* false s'il n'existait pas
*/
function delete(array $data, ?array $params=null): bool;
}

221
src/ldap/LdapAttr.php Normal file
View File

@ -0,0 +1,221 @@
<?php
namespace nulib\ldap;
use ArrayAccess;
use Countable;
use Iterator;
class LdapAttr implements ArrayAccess, Countable, Iterator {
use TIterableArray;
const MONOVALUED = 1, BINARY = 2, ORDERED = 4, NOT_HUMAN_READABLE = 8;
function __construct(string $name, ?array &$values, ?AbstractSyntax $syntax, ?int $flags) {
$this->name = $name;
$this->syntax = $syntax;
$this->flags = $flags;
$this->reset($values);
}
/** @var string */
protected $name;
function name(): string {
return $this->name;
}
/** @var ?array */
protected $data;
function reset(?array &$values): self {
$this->data =& $values;
return $this;
}
/** @var AbstractSyntax */
protected $syntax;
/** @var int */
protected $flags;
function isMonovalued(): bool {
return $this->flags !== null && $this->flags & self::MONOVALUED != 0;
}
function isBinary(): bool {
return $this->flags !== null && $this->flags & self::BINARY != 0;
}
function isOrdered(): bool {
return $this->flags !== null && $this->flags & self::ORDERED != 0;
}
function isNotHumanReadable(): bool {
return $this->flags !== null && $this->flags & self::NOT_HUMAN_READABLE != 0;
}
protected function fromLdap($value) {
$syntax = $this->syntax;
if ($syntax !== null) {
if ($this->isMonovalued()) $value = $syntax->fromMonovaluedLdap($value);
else $value = $syntax->fromMultivaluedLdap($value);
}
return $value;
}
protected function fromPhp($value): ?iterable {
$syntax = $this->syntax;
if ($syntax !== null) $value = $syntax->fromPhp($value);
else A::ensure_narray($value);
return $value;
}
/** retourner un tableau si multivalué, une valeur scalaire si monovalué */
function get($index=null) {
$value = $this->fromLdap($this->data);
if ($index !== null && is_array($value)) {
$value = array_key_exists($index, $value)? $value[$index]: null;
}
return $value;
}
/**
* retourner toutes les valeurs
*
* @param string $checkPrefixDel ne retourner que les valeurs qui commencent
* par ce préfixe ET enlever le préfixe
*/
function all(?string $checkPrefixDel=null): ?array {
if ($this->syntax === null) $values = $this->data;
else $values = $this->syntax->fromMultivaluedLdap($this->data);
if ($checkPrefixDel !== null && $values !== null) {
$filtered = [];
foreach ($values as $value) {
if (str::del_prefix($value, $checkPrefixDel)) {
$filtered[] = $value;
}
}
$values = $filtered;
}
return $values;
}
/** retourner la première valeur */
function first(?string $checkPrefixDel=null) {
return A::first($this->all($checkPrefixDel));
}
function set($values, bool $unlessNn=false): self {
if ($values instanceof LdapAttr) $values = $values->array();
if (!$unlessNn || $this->data === null) {
$this->data = $this->fromPhp($values);
}
return $this;
}
protected static function in_array(string $needle, array $haystack, bool $strict, ?int &$index=null): bool {
if (!$strict) $needle = strtolower($needle);
foreach ($haystack as $index => $hay) {
if ($strict && $hay === $needle) return true;
if (!$strict && strtolower($hay) == $needle) return true;
}
return false;
}
/** vérifier si la valeur spécifiée figure dans l'attribut */
function contains($value, bool $strict=false): bool {
$value = A::first($this->fromPhp($value));
if ($value === null || $this->data === null) return false;
return self::in_array($value, $this->data, $strict);
}
/**
* l'unicité est calculée ainsi:
* - en mode strict, ce doit être une égalité parfaite
* - en mode non strict, la comparaison est insensible à la casse
* XXX à terme, implémenter la comparaison en fonction de la syntaxe
*/
function add($value, bool $unique=true, bool $strict=false): self {
$value = A::first($this->fromPhp($value));
if ($value !== null) {
if (!$unique || $this->data === null ||
!self::in_array($value, $this->data, $strict)) {
$this->data[] = $value;
}
}
return $this;
}
function addAll(?iterable $values): self {
if ($values !== null) {
foreach ($values as $value) {
$this->add($value);
}
}
return $this;
}
function del($value, int $maxCount=-1, bool $strict=false): self {
if ($value !== null && $this->data !== null) {
$value = A::first($this->fromPhp($value));
$rekey = false;
while ($maxCount != 0) {
if (!self::in_array($value, $this->data, $strict, $index)) break;
unset($this->data[$index]);
$rekey = true;
if ($maxCount > 0) $maxCount--;
}
if ($rekey) $this->data = array_values($this->data);
}
return $this;
}
function ins(int $index, $value): self {
$value = A::first($this->fromPhp($value));
if ($value !== null) {
A::insert($this->data, $index, $value);
}
return $this;
}
function unset(int $index): self {
if ($this->data !== null) {
$count = count($this->array());
if ($count > 0 && $index < 0) {
while ($index < 0) $index += $count;
}
unset($this->data[$index]);
$this->data = array_values($this->data);
}
return $this;
}
function key() { return $this->_key(); }
function current() {
$current = $this->_current();
$syntax = $this->syntax;
if ($syntax !== null) $current = $syntax->ldap2php($current);
return $current;
}
#############################################################################
# données au format LDAP
function __toString() {
return implode("\n", $this->data);
}
/** retourner les données au format LDAP */
function &array(): ?array { return $this->data; }
function count(): int { return count($this->data); }
function keys(): array { return array_keys($this->data); }
function offsetExists($key) {
return $this->data !== null && array_key_exists($key, $this->data);
}
function offsetGet($key) { return array_key_exists($key, $this->data)? $this->data[$key]: null; }
function offsetSet($key, $value) { $this->data[$key] = $value; }
function offsetUnset($key) { unset($this->data[$key]); }
function __isset($key) { return $this->offsetExists($key); }
function __get($key) { return $this->offsetGet($key); }
function __set($key, $value) { $this->offsetSet($key, $value); }
function __unset($key) { $this->offsetUnset($key); }
}

434
src/ldap/LdapConn.php Normal file
View File

@ -0,0 +1,434 @@
<?php
namespace nulib\ldap;
/**
* 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");
}
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace nulib\ldap;
class LdapException extends UserException {
/** @param $r ?resource */
static function check(string $message, $r, $value, ?int $allow_errno=null) {
if ($value !== false) return $value;
if ($r !== null) {
$errno = ldap_errno($r);
if ($allow_errno !== null && $errno === $allow_errno) return $value;
throw new self($message, $errno, null, ldap_error($r));
} else {
throw new self($message);
}
}
static function check_result(string $message, $conn, $r) {
ldap_parse_result($conn, $r, $errorCode, $matchedDn, $errorMessage, $referrals, $controls);
if ($errorCode != 0) {
if (!$errorMessage) $errorMessage = ldap_err2str($errorCode);
throw new LdapException($message, $errorCode, $matchedDn, $errorMessage, $referrals, $controls);
}
}
function __construct(string $userMessage
, ?int $errorCode=null, ?string $matchedDn=null, ?string $errorMessage=null
, ?array $referrals=null, ?array $controls=null) {
if ($errorCode == 0) {
parent::__construct($userMessage);
} else {
$this->matchedDn = $matchedDn;
$this->errorMessage = $errorMessage;
$this->referrals = $referrals;
$this->controls = $controls;
$parts = ["error $errorCode"];
if ($errorMessage) $parts[] = $errorMessage;
if ($matchedDn) $parts[] = "matched_dn: $matchedDn";
if ($referrals) $parts[] = "referrals: ".implode(" ", $referrals);
$techMessage = implode(", ", $parts);
parent::__construct([
"user" => $userMessage,
"tech" => $techMessage,
], $errorCode);
}
}
/** @var string */
protected $matchedDn;
function getMatchedDn(): ?string {
return $this->matchedDn;
}
/** @var string */
protected $errorMessage;
function getErrorMessage(): ?string {
return $this->errorMessage;
}
/** @var ?array */
protected $referrals;
function getReferrals(): ?array {
return $this->referrals;
}
/** @var ?array */
protected $controls;
function getControls(): ?array {
return $this->controls;
}
}

372
src/ldap/LdapObject.php Normal file
View File

@ -0,0 +1,372 @@
<?php
namespace nulib\ldap;
use ArrayAccess;
use Countable;
/**
* 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"]];
}

215
src/ldap/LdapSearch.php Normal file
View File

@ -0,0 +1,215 @@
<?php
namespace nulib\ldap;
use IteratorAggregate;
use nulib\output\msg;
class LdapSearch extends Parametrable implements IteratorAggregate {
use Tparametrable;
static function parse_args(?array &$params, ?array $args
, ?string $searchbase=null, ?string $searchbase_exact=null
, ?string $scope=null): void {
$first = true;
$filter = null;
$attrs = null;
foreach ($args as $arg) {
if ($first) {
$first = false;
if (strpos($arg, "=") !== false) $filter = $arg;
else $attrs[] = $arg;
} else {
$attrs[] = $arg;
}
}
if ($filter !== null) $params["filter"] = $filter;
if ($attrs !== null) $params["attrs"] = $attrs;
if ($searchbase_exact !== null) {
$searchbase = $searchbase_exact;
$params["suffix"] = "";
}
if ($searchbase !== null) $params["searchbase"] = $searchbase;
if ($scope !== null) $params["scope"] = $scope;
}
const SCOPE_SUBTREE = 2, SCOPE_ONELEVEL = 1, SCOPE_BASE = 0;
const PARAMETRABLE_PARAMS_SCHEMA = [
"filter" => ["?content", "objectClass=*", "filtre de recherche"],
"attrs" => ["?array", [], "attributs à retourner"],
"searchbase" => ["?string", null, "DN de base pour la recherche"],
"scope" => ["?string", "sub", "étendue de la recherche"],
"suffix" => ["?string", null, "DN de base du serveur"],
"attributes_only" => ["bool", false, "faut-il ne retourner que les attributs?"],
"sizelimit" => ["int", -1, "limite de taille"],
"timelimit" => ["int", -1, "limite de temps"],
"deref" => ["int", LDAP_DEREF_NEVER, "type de déférencement"],
"controls" => ["array", [], "contrôles de la recherche"],
];
private static $search_md;
static function search_md(): Metadata {
return md_utils::ensure_md(self::$search_md, self::PARAMETRABLE_PARAMS_SCHEMA);
}
function __construct($conn, array $params) {
$this->conn = $conn;
parent::__construct($params);
}
/** @var resource */
protected $conn;
/** @var string */
protected $ppSearchbase;
/** @var string */
protected $filter;
function pp_setFilter($filter): void {
$this->filter = filters::parse($filter);
}
/** @var array */
protected $ppAttrs;
/** retourner la liste des attributs demandés */
function getAttrs(): array {
return $this->ppAttrs;
}
/** @var int */
protected $scope;
function pp_setScope(string $scope): void {
switch ($scope) {
case self::SCOPE_SUBTREE:
case "subtree":
case "sub":
case "s":
$this->scope = self::SCOPE_SUBTREE;
break;
case self::SCOPE_ONELEVEL:
case "onelevel":
case "one":
case "o":
$this->scope = self::SCOPE_ONELEVEL;
break;
case self::SCOPE_BASE:
case "base":
case "b":
$this->scope = self::SCOPE_BASE;
break;
default:
throw ValueException::invalid_value($scope, "scope");
}
}
/** @var string */
protected $ppSuffix;
/** @var bool */
protected $ppAttributesOnly;
/** @var int */
protected $ppSizelimit;
/** @var int */
protected $ppTimelimit;
/** @var int */
protected $ppDeref;
/** @var array */
protected $ppControls;
/** @throws LdapException */
function getIterator() {
$conn = $this->conn;
$args = [$conn];
$base = [];
if ($this->ppSearchbase) $base[] = $this->ppSearchbase;
if ($this->ppSuffix) $base[] = $this->ppSuffix;
$args[] = implode(",", $base);
A::merge($args, [
$this->filter?: "",
$this->ppAttrs?: [],
$this->ppAttributesOnly,
$this->ppSizelimit,
$this->ppTimelimit,
$this->ppDeref,
$this->ppControls,
]);
msg::debug("Searching searchbase=$args[1] filter=$args[2]");
$scope = $this->scope;
if ($scope == self::SCOPE_SUBTREE) $rr = @ldap_search(...$args);
elseif ($scope == self::SCOPE_ONELEVEL) $rr = @ldap_list(...$args);
elseif ($scope == self::SCOPE_BASE) $rr = @ldap_read(...$args);
else throw ValueException::invalid_value($scope, "scope");
$rr = LdapException::check("search", $conn, $rr, 32);
if ($rr === false) return; // pas trouvé
try {
$er = ldap_first_entry($conn, $rr);
while ($er !== false) {
$dn = ldap_get_dn($conn, $er);
$entry = ldap_get_attributes($conn, $er);
yield $dn => $entry;
$er = ldap_next_entry($conn, $er);
}
} catch (StopException $e) {
} finally {
ldap_free_result($rr);
}
}
/**
* retourner la première entrée du résultat de la recherche ou null si la
* recherche ne retourne aucun résultat
*
* @throws LdapException
*/
function first(?string &$dn=null): ?array {
$it = $this->getIterator();
$it->rewind();
if (!$it->valid()) return null;
try {
$dn = $it->key();
return $it->current();
} finally {
iter::close($it);
}
}
static function cook(array $initial_names, string $dn, array $entry): array {
# attributs demandés
$lkey2names = ["dn" => "dn"];
foreach ($initial_names as $name) {
if ($name == "+" || $name == "*") continue;
$lkey2names[strtolower($name)] = $name;
}
# attributs obtenus effectivement
$count = $entry["count"];
$attrs = ["dn" => [$dn]];
for ($i = 0; $i < $count; $i++) {
$name = $entry[$i];
$attr = $entry[$name];
unset($attr["count"]);
$attrs[$name] = $attr;
$lkey2names[strtolower($name)] = $name;
}
# ensuite, mettre à null les attributs qui n'ont pas été obtenus
foreach ($lkey2names as $name) {
if (!array_key_exists($name, $attrs)) {
$attrs[$name] = null;
}
}
# calculer les clés qui composent le DN
$dn_names = names::get_dn_names($dn, $lkey2names);
return [$attrs, $lkey2names, $dn_names];
}
}

10
src/ldap/LdapWalker.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace nulib\ldap;
/**
* Class LdapWalker: une classe permettant de parcourir les résultats d'une
* recherche
*/
class LdapWalker extends LdapObject implements ILdapWalker {
use TLdapWalker;
}

View File

@ -0,0 +1,24 @@
<?php
namespace nulib\ldap;
trait TCompositeValue {
use TArrayMd;
/** @var array */
private static $optional_keys;
protected function getOptionalKeys(): array {
$optionalKeys = self::$optional_keys;
if ($optionalKeys === null) {
$optionalKeys = self::$optional_keys = parent::getOptionalKeys();
}
return $optionalKeys;
}
function reset(?array $values): CompositeValue {
$this->md()->ensureSchema($values);
$this->data = $values;
return $this;
}
}

53
src/ldap/TLdapWalker.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace nulib\ldap;
use Iterator;
trait TLdapWalker {
function __construct(?LdapConn $conn=null, ?LdapSearch $search=null) {
parent::__construct(null, null, null, $conn);
if ($search !== null) $this->resetSearch($search);
}
/** @var LdapSearch */
protected $search;
function resetSearch(LdapSearch $search): ILdapWalker {
$this->close();
$this->reset(null, null, $search->getAttrs());
$this->search = $search;
return $this;
}
/** @var Iterator */
protected $it;
protected function loadNext(): bool {
$it = $this->it;
if (!$it->valid()) {
$this->close();
return false;
}
$this->load($it->key(), $it->current());
return true;
}
function next(?bool &$found=null): bool {
if ($this->it === null) {
$this->it = $this->search->getIterator();
$this->it->rewind();
$updateFound = true;
} else {
$this->it->next();
$updateFound = false;
}
$haveNext = $this->loadNext();
if ($updateFound) $found = $haveNext;
return $haveNext;
}
function close(): void {
iter::close($this->it);
$this->it = null;
}
}

315
src/ldap/consts.php Normal file
View File

@ -0,0 +1,315 @@
<?php
namespace nulib\ldap;
class consts {
/**
* @var array[] définitions connues des syntaxes, au cas le serveur ne les
* retourne pas
*/
const KNOWN_SLAPD_SYNTAXES = [
'1.3.6.1.4.1.1466.115.121.1.4' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.4',
'desc' => 'Audio',
'x_not_human_readable' => true,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.5' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.5',
'desc' => 'Binary',
'x_not_human_readable' => true,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.6' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.6',
'desc' => 'Bit String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.7' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.7',
'desc' => 'Boolean',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.8' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.8',
'desc' => 'Certificate',
'x_not_human_readable' => true,
'x_binary_transfer_required' => true,
],
'1.3.6.1.4.1.1466.115.121.1.9' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.9',
'desc' => 'Certificate List',
'x_not_human_readable' => true,
'x_binary_transfer_required' => true,
],
'1.3.6.1.4.1.1466.115.121.1.10' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.10',
'desc' => 'Certificate Pair',
'x_not_human_readable' => true,
'x_binary_transfer_required' => true,
],
'1.3.6.1.4.1.4203.666.11.10.2.1' => [
'oid' => '1.3.6.1.4.1.4203.666.11.10.2.1',
'desc' => 'X.509 AttributeCertificate',
'x_not_human_readable' => true,
'x_binary_transfer_required' => true,
],
'1.3.6.1.4.1.1466.115.121.1.12' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.12',
'desc' => 'Distinguished Name',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.2.36.79672281.1.5.0' => [
'oid' => '1.2.36.79672281.1.5.0',
'desc' => 'RDN',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.14' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.14',
'desc' => 'Delivery Method',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.15' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.15',
'desc' => 'Directory String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.22' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.22',
'desc' => 'Facsimile Telephone Number',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.23' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.23',
'desc' => 'Fax image',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.24' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.24',
'desc' => 'Generalized Time',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.25' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.25',
'desc' => 'Guide',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.26' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.26',
'desc' => 'IA5 String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.27' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.27',
'desc' => 'Integer',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.28' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.28',
'desc' => 'JPEG',
'x_not_human_readable' => true,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.34' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.34',
'desc' => 'Name And Optional UID',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.36' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.36',
'desc' => 'Numeric String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.38' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.38',
'desc' => 'OID',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.39' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.39',
'desc' => 'Other Mailbox',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.40' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.40',
'desc' => 'Octet String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.41' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.41',
'desc' => 'Postal Address',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.44' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.44',
'desc' => 'Printable String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.11' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.11',
'desc' => 'Country String',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.45' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.45',
'desc' => 'SubtreeSpecification',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.49' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.49',
'desc' => 'Supported Algorithm',
'x_not_human_readable' => true,
'x_binary_transfer_required' => true,
],
'1.3.6.1.4.1.1466.115.121.1.50' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.50',
'desc' => 'Telephone Number',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.51' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.51',
'desc' => 'Teletex Terminal Identifier',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.52' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.52',
'desc' => 'Telex Number',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.53' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.53',
'desc' => 'UTC Time',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.4.1.1466.115.121.1.54' => [
'oid' => '1.3.6.1.4.1.1466.115.121.1.54',
'desc' => 'LDAP Syntax Description',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.1.1.0.0' => [
'oid' => '1.3.6.1.1.1.0.0',
'desc' => 'RFC2307 NIS Netgroup Triple',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.1.1.0.1' => [
'oid' => '1.3.6.1.1.1.0.1',
'desc' => 'RFC2307 Boot Parameter',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
'1.3.6.1.1.16.1' => [
'oid' => '1.3.6.1.1.16.1',
'desc' => 'UUID',
'x_not_human_readable' => false,
'x_binary_transfer_required' => false,
],
];
const KNOWN_SYNTAX_CLASSES = [
'1.3.6.1.4.1.1466.115.121.1.4' => BinarySyntax::class, // audio
'1.3.6.1.4.1.1466.115.121.1.5' => BinarySyntax::class, // binary
'1.3.6.1.4.1.1466.115.121.1.6' => BinarySyntax::class, // bit string
'1.3.6.1.4.1.1466.115.121.1.7' => BooleanSyntax::class, // boolean
'1.3.6.1.4.1.1466.115.121.1.8' => BinarySyntax::class, // certificate
'1.3.6.1.4.1.1466.115.121.1.9' => BinarySyntax::class, // certificate list
'1.3.6.1.4.1.1466.115.121.1.10' => BinarySyntax::class, // certificate pair
'1.3.6.1.4.1.4203.666.11.10.2.1' => BinarySyntax::class, // X.509 AttributeCertificate
'1.3.6.1.4.1.1466.115.121.1.12' => StringSyntax::class, // DN
'1.2.36.79672281.1.5.0' => StringSyntax::class, // RDN
'1.3.6.1.4.1.1466.115.121.1.14' => StringSyntax::class, // delivery method
'1.3.6.1.4.1.1466.115.121.1.15' => StringSyntax::class, // directory string
'1.3.6.1.4.1.1466.115.121.1.22' => TelephoneSyntax::class, // fax number
'1.3.6.1.4.1.1466.115.121.1.24' => DateSyntax::class, // generalized time
'1.3.6.1.4.1.1466.115.121.1.26' => StringSyntax::class, // IA5 string
'1.3.6.1.4.1.1466.115.121.1.27' => IntegerSyntax::class, // integer
'1.3.6.1.4.1.1466.115.121.1.28' => BinarySyntax::class, // jpeg
'1.3.6.1.4.1.1466.115.121.1.34' => StringSyntax::class, // name and (opt.) oid
'1.3.6.1.4.1.1466.115.121.1.36' => IntegerSyntax::class, // numeric string
'1.3.6.1.4.1.1466.115.121.1.38' => StringSyntax::class, // oid
'1.3.6.1.4.1.1466.115.121.1.39' => MailSyntax::class, // other mailbox
'1.3.6.1.4.1.1466.115.121.1.40' => StringSyntax::class, // octet string
'1.3.6.1.4.1.1466.115.121.1.41' => PostalAddressSyntax::class, // postal address
'1.3.6.1.4.1.1466.115.121.1.44' => PrintableSyntax::class, // printable string
'1.3.6.1.4.1.1466.115.121.1.11' => StringSyntax::class, // country string
'1.3.6.1.4.1.1466.115.121.1.45' => StringSyntax::class, // subtree spec
'1.3.6.1.4.1.1466.115.121.1.49' => BinarySyntax::class, // supported algorithm
'1.3.6.1.4.1.1466.115.121.1.50' => TelephoneSyntax::class, // telephone number
'1.3.6.1.4.1.1466.115.121.1.52' => TelephoneSyntax::class, // telex number
'1.3.6.1.1.1.0.0' => StringSyntax::class, // RFC2307 NIS Netgroup Triple
'1.3.6.1.1.1.0.1' => StringSyntax::class, // RFC2307 Boot Parameter
'1.3.6.1.1.16.1' => StringSyntax::class, // uuid
];
const LDAP_CONTROL_CONSTANTS = [
# pas toutes ne sont définies en fonction de la version de PHP
"LDAP_CONTROL_MANAGEDSAIT",
"LDAP_CONTROL_PROXY_AUTHZ",
"LDAP_CONTROL_SUBENTRIES",
"LDAP_CONTROL_VALUESRETURNFILTER",
"LDAP_CONTROL_ASSERT",
"LDAP_CONTROL_PRE_READ",
"LDAP_CONTROL_POST_READ",
"LDAP_CONTROL_SORTREQUEST",
"LDAP_CONTROL_SORTRESPONSE",
"LDAP_CONTROL_PAGEDRESULTS",
"LDAP_CONTROL_SYNC",
"LDAP_CONTROL_SYNC_STATE",
"LDAP_CONTROL_SYNC_DONE",
"LDAP_CONTROL_DONTUSECOPY",
"LDAP_CONTROL_PASSWORDPOLICYREQUEST",
"LDAP_CONTROL_PASSWORDPOLICYRESPONSE",
"LDAP_CONTROL_X_INCREMENTAL_VALUES",
"LDAP_CONTROL_X_DOMAIN_SCOPE",
"LDAP_CONTROL_X_PERMISSIVE_MODIFY",
"LDAP_CONTROL_X_SEARCH_OPTIONS",
"LDAP_CONTROL_X_TREE_DELETE",
"LDAP_CONTROL_X_EXTENDED_DN",
"LDAP_CONTROL_VLVREQUEST",
"LDAP_CONTROL_VLVRESPONSE",
"LDAP_EXOP_MODIFY_PASSWD",
"LDAP_EXOP_REFRESH",
"LDAP_EXOP_START_TLS",
"LDAP_EXOP_TURN",
"LDAP_EXOP_WHO_AM_I",
"LDAP_CONTROL_AUTHZID_REQUEST",
"LDAP_CONTROL_AUTHZID_RESPONSE",
];
const ROOT_DSE_LITERALS = [
# Constantes non définies de façon normalisée
["1.3.6.1.1.8", "/*Cancel Extended Request*/ \"1.3.6.1.1.8\""],
["1.3.6.1.1.14", "/*Modify-Increment*/ \"1.3.6.1.1.14\""],
["1.3.6.1.4.1.4203.1.5.1", "/*All Op Attrs*/ \"1.3.6.1.4.1.4203.1.5.1\""],
["1.3.6.1.4.1.4203.1.5.2", "/*OC AD Lists*/ \"1.3.6.1.4.1.4203.1.5.2\""],
["1.3.6.1.4.1.4203.1.5.3", "/*LDAP Protocol Mechanism*/ \"1.3.6.1.4.1.4203.1.5.3\""],
["1.3.6.1.4.1.4203.1.5.4", "/*draft-zeilenga-ldap-rfc2596*/ \"1.3.6.1.4.1.4203.1.5.4\""],
["1.3.6.1.4.1.4203.1.5.5", "/*draft-zeilenga-ldap-rfc2596*/ \"1.3.6.1.4.1.4203.1.5.5\""],
];
}

96
src/ldap/filters.php Normal file
View File

@ -0,0 +1,96 @@
<?php
namespace nulib\ldap;
class filters {
private static function _escape(array $parts): string {
$op = false;
$first = true;
$fparts = [];
$index = 0;
foreach ($parts as $name => $part) {
if ($first) {
$first = false;
switch ($part) {
case "&": case "and": $op = "&"; break;
case "|": case "or": $op = "|"; break;
case "!": case "not": $op = "!"; break;
}
if ($op) {
if ($index === $name) $index++;
continue;
}
}
if ($index === $name) {
# séquentiel
$index++;
if (is_array($part)) {
$fparts[] = self::_escape($part);
} else {
str::add_prefix($part, "(");
str::add_suffix($part, ")");
$fparts[] = $part;
}
} else {
# associatif
$name = ldap_escape($name, "", LDAP_ESCAPE_FILTER);
foreach (A::with($part) as $value) {
$value = ldap_escape($value, "", LDAP_ESCAPE_FILTER);
$fparts[] = "($name=$value)";
}
}
}
$filter = implode("", $fparts);
if (count($fparts) > 1 || $op === "!") {
if (!$op) $op = "&";
$filter = "($op$filter)";
}
return $filter;
}
static function parse($filter): string {
if (!$filter) $filter = "objectClass=*";
return self::_escape(A::with($filter));
}
static function not(string $filter): string {
str::add_prefix($filter, "(");
str::add_suffix($filter, ")");
return "(!$filter)";
}
/** mettre en échappement ($attr$op$value) en ignorant les wildcards */
private static function _filter(string $name, string $op, string $value): string {
$name = ldap_escape($name, "*", LDAP_ESCAPE_FILTER);
$value = ldap_escape($value, "*", LDAP_ESCAPE_FILTER);
return "($name$op$value)";
}
static function exists(string $name): string {
return self::_filter($name, "=", "*");
}
static function eq(string $name, string $value): string {
return self::_filter($name, "=", $value);
}
static function ge(string $name, string $value): string {
return self::_filter($name, ">=", $value);
}
static function le(string $name, string $value): string {
return self::_filter($name, "<=", $value);
}
static function gt(string $name, string $value): string {
return self::not(self::le($name, $value));
}
static function lt(string $name, string $value): string {
return self::not(self::ge($name, $value));
}
static function approx(string $name, string $value): string {
return self::_filter($name, "~=", $value);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace nulib\ldap\io;
use nulib\file;
use nulib\file\IWriter;
use nulib\ldap\LdapObject;
abstract class LdapWriter {
static function write_object($output, LdapObject $object, ?array $names=null): void {
$writer = new static($output);
$writer->write($object, $names);
$writer->close();
}
function __construct($output=null) {
$this->writer = file::writer($output);
}
/** @var IWriter */
protected $writer;
function close(): void {
$this->writer->close();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace nulib\ldap\io;
use nulib\ldap\LdapObject;
/**
* Class LdifWriter
*/
class LdifWriter extends LdapWriter {
function write(?LdapObject $object, ?array $names=null): self {
if ($object !== null) {
$writer = $this->writer;
if ($names === null) $names = $object->keys();
if (!in_array("dn", $names)) {
array_unshift($names, "dn");
}
foreach ($names as $name) {
$values = $object->_get($name)->array();
if ($values !== null) {
foreach ($values as $value) {
$writer->fwrite("$name: $value\n");
}
}
}
$writer->fwrite("\n");
}
return $this;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace nulib\ldap\io;
use nulib\ext\yaml;
use nulib\ldap\LdapObject;
/**
* Class YamlWriter
*/
class YamlWriter extends LdapWriter {
function write(?LdapObject $object, ?array $names=null): self {
if ($object !== null) {
if ($names === null) $names = $object->keys();
if (!in_array("dn", $names)) {
array_unshift($names, "dn");
}
$values = [];
foreach ($names as $name) {
$value = $object->all($name);
if (count($value) == 1) $value = $value[0];
$values[$name] = $value;
}
$writer = $this->writer;
$writer->fwrite(yaml::with($values));
$writer->fwrite("\n");
}
return $this;
}
}

22
src/ldap/labels.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace nulib\ldap;
class labels {
static function strip(?string &$value, ?string &$label=null): void {
if ($value === null) return;
if (preg_match('/^(\{[^}]*})/', $value, $ms, PREG_OFFSET_CAPTURE)) {
$label = $ms[1][0];
$value = substr($value, $ms[1][1] + strlen($label));
}
}
static function add(?string $value, ?string $label): ?string {
if ($value === null) return null;
if ($label !== null) {
str::add_prefix($label, "{");
str::add_suffix($label, "}");
}
return "$label$value";
}
}

157
src/ldap/ldap.php Normal file
View File

@ -0,0 +1,157 @@
<?php
namespace nulib\ldap;
class ldap {
#############################################################################
const ADD_SCHEMA = [
"controls" => ["array", []],
];
/** @var Metadata */
private static $add_md;
static function add_md(): Metadata {
return md_utils::ensure_md(self::$add_md, self::ADD_SCHEMA);
}
static function add($conn, string $dn, array $attrs, $params=null): void {
self::add_md()->ensureSchema($params);
$r = LdapException::check("add", $conn
, @ldap_add_ext($conn, $dn, $attrs, $params["controls"]));
LdapException::check_result("add", $conn, $r);
}
#############################################################################
const MODIFY_SCHEMA = [
"controls" => ["array", []],
];
/** @var Metadata */
private static $modify_md;
static function modify_md(): Metadata {
return md_utils::ensure_md(self::$modify_md, self::MODIFY_SCHEMA);
}
static function prepare_modify(array $modattrs): array {
$modifs = [];
foreach ($modattrs as $modattr) {
$modtype = false;
$first = true;
$index = 0;
foreach ($modattr as $name => $value) {
if ($first && $name === $index) {
$first = false;
$index++;
switch ($value) {
case "add":
$modtype = LDAP_MODIFY_BATCH_ADD;
break;
case "delete":
$modtype = LDAP_MODIFY_BATCH_REMOVE;
break;
case "replace":
$modtype = LDAP_MODIFY_BATCH_REPLACE;
break;
}
continue;
}
if ($name === $index) {
$index++;
$modifs[] = [
"modtype" => LDAP_MODIFY_BATCH_REMOVE_ALL,
"attrib" => $value,
];
} else {
$modifs[] = [
"modtype" => $modtype,
"attrib" => $name,
"values" => $value
];
}
}
}
return $modifs;
}
static function modify($conn, string $dn, array $modattrs, $params=null): void {
self::modify_md()->ensureSchema($params);
$modifs = self::prepare_modify($modattrs);
LdapException::check("modify", $conn
, @ldap_modify_batch($conn, $dn, $modifs, $params["controls"]));
}
#############################################################################
const RENAME_SCHEMA = [
"new_parent" => ["?string", null],
"delete_old_rdn" => ["bool", true],
"controls" => ["array", []],
];
/** @var Metadata */
private static $rename_md;
static function rename_md(): Metadata {
return md_utils::ensure_md(self::$rename_md, self::RENAME_SCHEMA);
}
/**
* préparer les paramètres pour le renommage
*
* si $newRdn n'est pas vide:
* - si $params["new_parent"] n'est pas spécifié ou null, alors on ne fait
* qu'un renommage: prendre le suffixe de $dn
* - sinon, le nouveau DN est "$newRdn,$params[new_parent]"
*
* si $newRdn est vide:
* - il s'agit d'un déplacement de branche. $params["new_parent"] ne doit pas
* être vide et c'est la nouvelle destination. le RDN n'est pas modifié
*/
static function prepare_rename(string $dn, string &$newRdn, &$params = null): bool {
self::rename_md()->ensureSchema($params);
names::split_dn($dn, $origRdn, $origParent);
$newParent = $params["new_parent"];
if ($newRdn != "") {
# renommage et éventuellement déplacement
if (strpos($newRdn, "=") === false) {
# si le rdn ne comporte que la valeur, alors prendre le nom de
# l'attribut depuis origRdn
$name = A::first_key(names::split_rdn($origRdn));
$newRdn = names::build_rdn($name, $newRdn);
}
if ($newParent === null) $newParent = $origParent;
} else {
# déplacement avec le même RDN
$newRdn = $origRdn;
}
$newDn = names::join($newRdn, $newParent);
names::split_dn($newDn, $newRdn, $newParent);
$params["new_parent"] = $newParent;
return $newDn !== $dn;
}
static function rename($conn, string $dn, string $newRdn, array $params): string {
$newParent = $params["new_parent"];
$r = LdapException::check("rename", $conn
, @ldap_rename_ext($conn, $dn, $newRdn, $newParent
, $params["delete_old_rdn"], $params["controls"]));
LdapException::check_result("rename", $conn, $r);
return names::join($newRdn, $newParent);
}
#############################################################################
const DELETE_SCHEMA = [
"controls" => ["array", []],
];
/** @var Metadata */
private static $delete_md;
static function delete_md(): Metadata {
return md_utils::ensure_md(self::$delete_md, self::DELETE_SCHEMA);
}
static function delete($conn, string $dn, $params=null): void {
self::delete_md()->ensureSchema($params);
$r = LdapException::check("delete", $conn
, @ldap_delete_ext($conn, $dn, $params["controls"]));
LdapException::check_result("delete", $conn, $r);
}
}

31
src/ldap/ldap_config.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace nulib\ldap;
class ldap_config {
static function get_shared_file(string $uri): string {
if ($uri == "ldapi://") {
$file = "ldapi__.ldaphost";
} else {
$parts = parse_url($uri);
if ($parts === false) throw ValueException::invalid_value($uri, "uri");
$scheme = A::get($parts, "scheme", "ldap");
$host = A::get($parts, "host");
$port = A::get($parts, "port");
if ($port === null) {
if ($scheme === "ldap") $port = 389;
elseif ($scheme === "ldaps") $port = 636;
}
$file = "${scheme}_${host}_${port}.ldaphost";
}
return $file;
}
static function get_file(string $file, ?string $profile=null): string {
if (!path::is_qualified($file) && !path::have_ext($file)) {
if ($profile !== null) $file .= ".$profile";
$file .= ".ldapconf";
}
return $file;
}
}

61
src/ldap/ldap_server.php Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace nulib\ldap;
use nulib\output\msg;
abstract class ldap_server {
const NAME = null;
protected static function name(?string $suffix=null): string {
$name = static::NAME;
if ($suffix !== null) {
$name .= "_";
$name .= $suffix;
}
return $name;
}
private static function map_profile(string $profile): string {
$profile_map = config::k(self::name("profile_map"));
return A::get($profile_map, $profile, $profile);
}
static $profile;
/** obtenir le profil LDAP courant */
static function get_profile(): string {
$profile = self::$profile;
if ($profile === null) {
if ($profile === null) $profile = config::k(self::name("profile"));
if ($profile === null) $profile = self::map_profile(config::get_profile());
self::$profile = $profile;
}
return $profile;
}
/** spécifier le profil LDAP courant */
static function set_profile(?string $profile): void {
if ($profile === null) $profile = config::get_profile();
self::$profile = self::map_profile($profile);
}
/** adapter le chemin vers le fichier de configuration */
protected static function fix_path(string $config): string {
return $config;
}
static function conn(?array $config=null, ?string $profile=null): LdapConn {
if ($profile === null) $profile = self::get_profile();
$name = self::name();
msg::debug("Profil $name: $profile");
$configFile = static::fix_path(ldap_config::get_file($name, $profile));
if (!file_exists($configFile)) {
$configname = path::filename($configFile);
throw new ValueException("$name: profil LDAP invalide (fichier '$configname' non trouvé)");
}
return new LdapConn(array_merge(...SL::filter_n([
require $configFile,
$config,
])));
}
}

89
src/ldap/names.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace nulib\ldap;
class names {
static function split_dn(string $dn, ?string &$rdn, ?string &$parent_dn): bool {
$dparts = ldap_explode_dn($dn, 0);
$count = $dparts["count"];
if ($count > 0) {
$rdn = $dparts[0];
$sparts = [];
for ($i = 1; $i < $count; $i++) {
$sparts[] = $dparts[$i];
}
$parent_dn = implode(",", $sparts);
return true;
}
return false;
}
static function ldap_unescape($string) {
$hex2bin = function ($ms) {
$m = array_shift($ms);
return hex2bin(substr($m, 1));
};
return preg_replace_callback('/\\\\[0-9a-fA-F]{2}/', $hex2bin, $string);
}
static function split_rdn(string $rdn): array {
$attrs = [];
$rparts = explode("+", $rdn);
foreach ($rparts as $rpart) {
if (strpos($rpart, "=") === false) {
throw ValueException::invalid_value($rdn, "rdn");
}
[$name, $value] = explode("=", $rpart, 2);
$name = self::ldap_unescape($name);
$value = self::ldap_unescape($value);
$attrs[$name][] = $value;
}
return $attrs;
}
static function build_rdn(string $name, string $value): string {
$name = ldap_escape($name, 0, LDAP_ESCAPE_DN);
$value = ldap_escape($value, 0, LDAP_ESCAPE_DN);
return "$name=$value";
}
static function get_dn_names(?string $dn, ?array $lkeys2names=null): ?array {
$dn_names = null;
if ($dn !== null) {
$dn_names = [];
if (self::split_dn($dn, $rdn, $parent_dn)) {
foreach (array_keys(self::split_rdn($rdn)) as $name) {
$dn_names[] = A::get($lkeys2names, strtolower($name), $name);
}
}
}
return $dn_names;
}
static function join($rdn, string $parent_dn): string {
if (is_array($rdn)) {
$rparts = [];
foreach ($rdn as $name => $values) {
$name = ldap_escape($name, 0, LDAP_ESCAPE_DN);
foreach (A::with($values) as $value) {
$value = ldap_escape($value, 0, LDAP_ESCAPE_DN);
$rparts[] = "$name=$value";
}
}
$rdn = implode("+", $rparts);
}
$dparts = [];
if ($rdn) $dparts[] = $rdn;
if ($parent_dn) $dparts[] = $parent_dn;
return implode(",", $dparts);
}
/** tester si $dn a le suffixe $suffix */
static function have_suffix(string $dn, string $suffix): bool {
$dparts = ldap_explode_dn($dn, 0);
$sparts = ldap_explode_dn($suffix, 0);
$count = $sparts["count"];
return array_slice($dparts, -$count) === array_slice($sparts, -$count);
}
}

30
src/ldap/scheman.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace nulib\ldap;
/**
* Class scheman: gestionnaire de schéma global partagé
*
* Cette classe ne peut être utilisée correctement que pour une seule instance
* de {@link LdapConn}
*/
class scheman {
/** @var SchemaManager */
protected static $scheman;
static function init(LdapConn $conn, ?array $overrides=null): void {
self::$scheman = new SchemaManager($conn, $overrides);
}
static function autogen_schema(array $objectClasses): array {
return self::$scheman->autogenSchema($objectClasses);
}
static function autogen_properties(array $schema): array {
return self::$scheman->autogenProperties($schema);
}
static function autogen_methods(array $schema): array {
return self::$scheman->autogenMethods($schema);
}
}

View File

@ -0,0 +1,245 @@
<?php
namespace nulib\ldap\schemas;
use nulib\A;
use nulib\cl;
use nulib\ldap\consts;
use nulib\ldap\LdapAttr;
use nulib\ldap\LdapConn;
use nulib\ldap\syntaxes\BinarySyntax;
use nulib\ldap\syntaxes\StringSyntax;
/**
* Class LdapSchemaExtractor: extracteur de schéma LDAP, pour utilisation avec
* PHP
*/
class LdapSchemaExtractor {
function __construct(?array $schemaInfos=null) {
if ($schemaInfos !== null) {
[
"ldap_syntaxes" => $this->ldapSyntaxes,
"attribute_types" => $this->attributeTypes,
"object_classes" => $this->objectClasses,
] = $schemaInfos;
}
}
protected $ldapSyntaxes;
protected $attributeTypes;
protected $objectClasses;
function loadSchema(LdapConn $conn): array {
$schema = null;
$schemaDn = $conn->getRootDse()->first("subschemaSubentry");
if ($schemaDn !== null) {
$schema = $conn->empty()->load($schemaDn, $conn->_search($schemaDn, [
"suffix" => "",
"attrs" => [
"ldapSyntaxes",
"attributeTypes",
"objectClasses",
],
"scope" => "base",
])->first());
}
if ($schema === null) {
throw new IllegalAccessException("unable to find subschemaSubentry attribute");
}
$parser = new LseSyntax();
$ldapSyntaxes = [];
foreach ($schema->get("ldapSyntaxes", []) as $ldapSyntax) {
$ldapSyntax = $parser->parse($ldapSyntax);
$ldapSyntaxes[$ldapSyntax["oid"]] = $ldapSyntax;
}
$parser = new LseAttribute();
$attributeTypes = [];
foreach ($schema->get("attributeTypes", []) as $attributeType) {
$attributeType = $parser->parse($attributeType);
$attributeTypes[$attributeType["oid"]] = $attributeType;
}
$parser = new LseObjectClass();
$objectClasses = [];
foreach ($schema->get("objectClasses", []) as $objectClass) {
$objectClass = $parser->parse($objectClass);
$objectClasses[$objectClass["oid"]] = $objectClass;
}
return [
"ldap_syntaxes" => $this->ldapSyntaxes = $ldapSyntaxes,
"attribute_types" => $this->attributeTypes = $attributeTypes,
"object_classes" => $this->objectClasses = $objectClasses,
];
}
protected $syntaxes;
protected $attributes;
protected $canonAttrs;
protected $classes;
protected $canonClasses;
function init(): array {
## calculer la liste des syntaxes, et les classer par OID
$ldapSyntaxes = $this->ldapSyntaxes;
# rajouter une liste connue de syntaxes
A::merge($ldapSyntaxes, consts::KNOWN_SLAPD_SYNTAXES);
$syntaxes = [];
foreach ($ldapSyntaxes as $syntax) {
$oid = $syntax["oid"];
# si la syntaxe a déjà été définie, ignorer
if (array_key_exists($oid, $syntaxes)) continue;
$class = cl::get(consts::KNOWN_SYNTAX_CLASSES, $oid);
if ($class === null) {
$binary = $syntax["x_not_human_readable"] || $syntax["x_binary_transfer_required"];
$class = $binary? BinarySyntax::class: StringSyntax::class;
}
$syntax["class"] = $class;
$syntaxes[$oid] = $syntax;
}
## calculer la liste des attributs, et les classer par nom canonique
$attributes = [];
$canonAttrs = [];
foreach ($this->attributeTypes as $attribute) {
$names = $attribute["names"];
$canonName = $names[0];
$attribute["name"] = $canonName;
foreach ($names as $name) {
$canonAttrs[strtolower($name)] = $canonName;
}
$attribute["class"] = cl::pget($syntaxes, [$attribute["syntax"], "class"]);
$attributes[strtolower($canonName)] = $attribute;
}
# résoudre l'héritage des attributs
foreach ($attributes as &$attribute) {
foreach ($attribute["sups"] as $sup) {
$sup = strtolower(cl::get($canonAttrs, strtolower($sup), $sup));
A::update_n($attribute, $attributes[$sup]);
}
}; unset($attribute);
# puis mettre à false les valeurs booléennes nulles
foreach ($attributes as &$attribute) {
foreach (LseAttribute::BOOL_ATTRS as $name) {
$attribute[$name] = boolval($attribute[$name]);
}
}; unset($attribute);
## calculer la liste des classes, et les classer par nom canonique.
## les noms des attributs sont aussi canonisés
$classes = [];
$canonClasses = [];
foreach ($this->objectClasses as $class) {
$names = $class["names"];
$canonName = $names[0];
$class["name"] = $canonName;
foreach ($names as $name) {
$canonClasses[strtolower($name)] = $canonName;
}
$musts = cl::with($class["musts"]);
foreach ($musts as &$name) {
$name = cl::get($canonAttrs, strtolower($name), $name);
}; unset($name);
$class["musts"] = $musts;
$mays = cl::with($class["mays"]);
foreach ($mays as &$name) {
$name = cl::get($canonAttrs, strtolower($name), $name);
}; unset($name);
$class["mays"] = $mays;
$class["attrs"] = array_merge($musts, $mays);
$classes[strtolower($canonName)] = $class;
}
# résoudre l'héritage des classes
foreach ($classes as &$class) {
foreach ($class["sups"] as $sup) {
$sup = strtolower(cl::get($canonAttrs, strtolower($sup), $sup));
$sup = $classes[$sup];
A::update_n($class, $sup);
A::merge($class["musts"], $sup["musts"]);
A::merge($class["mays"], $sup["mays"]);
}
}; unset($class);
## fin de l'initialisation
return [
"syntaxes" => $this->syntaxes = $syntaxes,
"attributes" => $this->attributes = $attributes,
"canon_attrs" => $this->canonAttrs = $canonAttrs,
"classes" => $this->classes = $classes,
"canon_classes" => $this->canonClasses = $canonClasses,
];
}
const getAttributes_overrides_SCHEMA = [
"name" => "string",
"class" => "?string",
"set" => "?int",
"reset" => "?int",
];
/** @var Metadata */
private static $getAttributes_overrides_md;
function getAttributes(array $objectClasses, ?array $overrides=null): array {
if ($overrides !== null) {
$tmp = [];
foreach ($overrides as $name => $override) {
$attribute = ValueException::check_nn(
cl::get($this->attributes, strtolower($name))
, "$name: attribut non défini");
$tmp[$attribute["name"]] = $override;
}
$overrides = $tmp;
$md = md_utils::ensure_md(self::$getAttributes_overrides_md, self::getAttributes_overrides_SCHEMA);
$md->eachEnsureSchema($overrides);
}
$nameRequired = [];
foreach ($objectClasses as $name) {
$name = cl::get($this->canonClasses, strtolower($name), $name);
$class = ValueException::check_nn(
cl::get($this->classes, strtolower($name))
, "$name: classe non définie");
foreach ($class["musts"] as $must) {
$nameRequired[$must] = true;
}
foreach ($class["mays"] as $may) {
A::replace_nx($nameRequired, $may, false);
}
}
$attributes = [
"dn" => [
"name" => "dn",
"class" => StringSyntax::class,
"flags" => LdapAttr::MONOVALUED,
],
];
foreach ($nameRequired as $name => $required) {
$lname = strtolower($name);
$attribute = ValueException::check_nn(
cl::get($this->attributes, $lname)
, "$name: attribut non défini");
$syntax = ValueException::check_nn(
cl::get($this->syntaxes, $attribute["syntax"])
, "$attribute[syntax]: syntaxe non définie");
$class = $attribute["class"];
$monovalued = $attribute["single_value"]? LdapAttr::MONOVALUED: 0;
$binary = $syntax["x_binary_transfer_required"]? LdapAttr::BINARY: 0;
$ordered = $attribute["x_ordered"]? LdapAttr::ORDERED: 0;
$notHumanReadable = $syntax["x_not_human_readable"]? LdapAttr::NOT_HUMAN_READABLE: 0;
$flags = $monovalued + $binary + $ordered + $notHumanReadable;
$override = cl::get($overrides, $name);
if ($override !== null) {
if ($override["class"] !== null) $class = $override["class"];
if ($override["set"] !== null) $flags = $flags | $override["set"];
if ($override["reset"] !== null) $flags = $flags & ~$override["reset"];
}
$attributes[$lname] = [
"name" => $name,
"class" => $class,
"flags" => $flags,
];
}
return $attributes;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace nulib\ldap\schemas;
use nulib\output\log;
class LseAttribute extends LseParser {
protected $data;
const BOOL_ATTRS = [
"single_value",
"no_user_modification",
"x_ordered",
"obsolete",
];
protected function reset(): array {
return $this->data = [
"oid" => null,
"names" => [],
"desc" => null,
"sups" => [],
"equality" => null,
"substr" => null,
"ordering" => null,
"syntax" => null,
"single_value" => null,
"no_user_modification" => null,
"usage" => null,
"x_ordered" => null,
"x_origin" => null,
"obsolete" => null,
];
}
function parse(?string $s=null): array {
if ($s !== null) $this->s = $s;
$data = $this->reset();
$this->skipLiteral('(');
$data["oid"] = self::fix_oid($this->parseName());
while ($this->isName()) {
$okey = $this->parseName();
$key = str_replace("-", "_", strtolower($okey));
switch ($key) {
case "name":
$data["${key}s"] = $this->parseStrings();
break;
case "sup":
$data["${key}s"] = $this->parseNames();
break;
case "desc":
case "x_ordered":
case "x_origin":
$data[$key] = $this->parseString();
break;
case "equality":
case "substr":
case "ordering":
case "usage":
$data[$key] = $this->parseName();
break;
case "syntax":
$data[$key] = self::fix_oid($this->parseName());
break;
case "single_value":
case "no_user_modification":
case "obsolete":
$data[$key] = true;
break;
default:
log::warning("unknown key $okey in |$s|");
$data["unknown_keys"][] = $okey;
break;
}
}
$this->skipLiteral(')');
# ne pas mettre de suite les valeurs false: elle sont mises à jour dans
# LdapSchemaExtractor
## puis mettre à jour les valeurs booléennes
#foreach (self::BOOL_ATTRS as $name) {
# $data[$name] = boolval($data[$name]);
#}
return $data;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace nulib\ldap\schemas;
use nulib\output\log;
class LseObjectClass extends LseParser {
const BOOL_ATTRS = [];
protected $data;
protected function reset(): array {
return $this->data = [
"oid" => null,
"names" => [],
"desc" => null,
"sups" => [],
"type" => null,
"musts" => null,
"mays" => null,
];
}
function parse(?string $s=null): array {
if ($s !== null) $this->s = $s;
$data = $this->reset();
$this->skipLiteral('(');
$data["oid"] = self::fix_oid($this->parseName());
while ($this->isName()) {
$okey = $this->parseName();
$key = str_replace("-", "_", strtolower($okey));
switch ($key) {
case "name":
$data["${key}s"] = $this->parseStrings();
break;
case "sup":
case "must":
case "may":
$data["${key}s"] = $this->parseNames();
break;
case "desc":
$data[$key] = $this->parseString();
break;
case "abstract":
case "structural":
case "auxiliary":
$data["type"] = $key;
break;
default:
log::warning("unknown key $okey in |$s|");
$data["unknown_keys"][] = $okey;
break;
}
}
$this->skipLiteral(')');
# puis mettre à jour les valeurs booléennes
foreach (self::BOOL_ATTRS as $name) {
$data[$name] = boolval($data[$name]);
}
return $data;
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace nulib\ldap\schemas;
use nulib\ValueException;
class LseParser {
/** supprimer le {size} à la fin d'un OID */
protected static function fix_oid(string $oid): string {
return preg_replace('/\{\d+}$/', "", $oid);
}
function __construct(?string $s=null) {
$this->s = $s;
}
protected function expected(string $expected): ValueException {
return new ValueException("expected $expected, got $this->s");
}
protected function unexpected(string $value): ValueException {
return new ValueException("unexpected $value");
}
protected $s;
#~~~~
const SPACES_PATTERN = '/^\s+/';
protected function skipSpaces(): void {
if (preg_match(self::SPACES_PATTERN, $this->s, $ms)) {
$this->s = substr($this->s, strlen($ms[0]));
}
}
#~~~~
protected function isLiteral(string $literal): bool {
return substr($this->s, 0, strlen($literal)) === $literal;
}
protected function skipLiteral(string $literal): void {
$pos = strlen($literal);
if (substr($this->s, 0, $pos) === $literal) {
$this->s = substr($this->s, $pos);
} else {
throw $this->expected($literal);
}
$this->skipSpaces();
}
#~~~~
const NAME_PATTERN = '/^\S+/';
protected function isName(): bool {
if (!preg_match(self::NAME_PATTERN, $this->s, $ms)) return false;
$name = $ms[0];
return !in_array($name, ['(', ')', '$']);
}
protected function parseName(): string {
if (!preg_match(self::NAME_PATTERN, $this->s, $ms)) {
throw $this->expected("<NAME>");
}
$name = $ms[0];
$this->s = substr($this->s, strlen($name));
$this->skipSpaces();
return $name;
}
#~~~~
const STRING_PATTERN = "/^'([^']*)'/";
protected function isString(): bool {
return preg_match(self::STRING_PATTERN, $this->s, $ms);
}
protected function parseString(): string {
if (!preg_match(self::STRING_PATTERN, $this->s, $ms)) {
throw $this->expected("<STRING>");
}
$this->s = substr($this->s, strlen($ms[0]));
$this->skipSpaces();
return $ms[1];
}
#~~~~
protected function parseNames(): array {
if ($this->isName()) return [$this->parseName()];
$names = [];
if ($this->isLiteral('(')) {
$this->skipLiteral('(');
while ($this->isName()) {
$names[] = $this->parseName();
if ($this->isLiteral('$')) $this->skipLiteral('$');
}
$this->skipLiteral(')');
} else {
$names[] = $this->parseName();
}
return $names;
}
protected function parseStrings(): array {
if ($this->isString()) return [$this->parseString()];
$strings = [];
if ($this->isLiteral('(')) {
$this->skipLiteral('(');
while ($this->isString()) {
$strings[] = $this->parseString();
}
$this->skipLiteral(')');
} else {
$strings[] = $this->parseString();
}
return $strings;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace nulib\ldap\schemas;
use nulib\output\log;
class LseSyntax extends LseParser {
const BOOL_ATTRS = [
"x_not_human_readable",
"x_binary_transfer_required",
];
protected $data;
protected function reset(): array {
return $this->data = [
"oid" => null,
"desc" => null,
"x_not_human_readable" => null,
"x_binary_transfer_required" => null,
];
}
function parse(?string $s=null): array {
if ($s !== null) $this->s = $s;
$data =$this->reset();
$this->skipLiteral('(');
$data["oid"] = self::fix_oid($this->parseName());
while ($this->isName()) {
$okey = $this->parseName();
$key = str_replace("-", "_", strtolower($okey));
switch ($key) {
case "desc":
$data[$key] = $this->parseString();
break;
case "x_not_human_readable":
case "x_binary_transfer_required":
$data[$key] = boolval($this->parseString());
break;
default:
log::warning("unknown key $okey in $s");
$data["unknown_keys"][] = $okey;
break;
}
}
$this->skipLiteral(')');
# puis mettre à jour les valeurs booléennes
foreach (self::BOOL_ATTRS as $name) {
$data[$name] = boolval($data[$name]);
}
return $this->data = $data;
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace nulib\ldap\schemas;
use nulib\ldap\LdapAttr;
use nulib\ldap\LdapConn;
use nulib\ldap\syntaxes\AbstractSyntax;
use nulib\ldap\syntaxes\CompositeSyntax;
class SchemaManager {
function __construct(LdapConn $conn, ?array $overrides=null) {
$lse = new LdapSchemaExtractor($conn->getSchemaInfos());
$lse->init();
$this->lse = $lse;
$this->overrides = $overrides;
}
/** @var LdapSchemaExtractor */
protected $lse;
/** @var array|null */
protected $overrides;
function getAttributes(array $objectClasses): array {
return $this->lse->getAttributes($objectClasses, $this->overrides);
}
/** @var AbstractSyntax[] */
protected $syntaxes;
function getSyntax($class): AbstractSyntax {
if (is_array($class)) return func::cons(...$class);
$syntax = A::get($this->syntaxes, $class);
if ($syntax === null) {
$syntax = $this->syntaxes[$class] = func::cons($class);
}
return $syntax;
}
function autogenSchema(array $objectClasses): array {
return $this->getAttributes($objectClasses);
}
static function fix_type(AbstractSyntax $syntax, bool $monovalued): array {
if ($syntax instanceof CompositeSyntax) {
if ($monovalued) $phpType = $syntax->getPhpType();
else $phpType = $syntax->getAttrClass();
} else {
$phpType = $syntax->getPhpType();
if (!$monovalued) $phpType .= "[]";
}
return Autogen::fix_type($phpType);
}
function autogenProperties(array $schema): array {
$properties = [];
foreach ($schema as $attribute) {
$name = $attribute["name"];
/** @var AbstractSyntax $syntax */
$syntax = $this->getSyntax($attribute["class"]);
$monovalued = ($attribute["flags"] & LdapAttr::MONOVALUED) != 0;
[$phpType, $returnType] = self::fix_type($syntax, $monovalued);
$properties[] = "$returnType \$$name";
}
return $properties;
}
function autogenMethods(array $schema): array {
$methods = [];
foreach ($schema as $attribute) {
$name = $attribute["name"];
/** @var AbstractSyntax $syntax */
$syntax = $this->getSyntax($attribute["class"]);
$returnType = $syntax instanceof CompositeSyntax? $syntax->getAttrClass(): LdapAttr::class;
$methods[] = "\\$returnType $name()";
}
return $methods;
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace nulib\ldap\syntaxes;
use nulib\A;
use nulib\cl;
use nulib\ldap\LdapAttr;
use nulib\ldap\LdapConn;
abstract class AbstractSyntax {
/** @var LdapConn */
protected $conn;
function initConn(LdapConn $conn) {
$this->conn = $conn;
}
function newAttr(string $name, ?array &$values, ?int $flags): LdapAttr {
return new LdapAttr($name, $values, $this, $flags);
}
function getPhpType(): ?string {
return "string";
}
/** @throws SyntaxException si $value est invalide */
abstract function php2ldap($value): ?string;
abstract function ldap2php(string $value);
/** transformer les valeurs d'un attribut LDAP en PHP */
function fromMultivaluedLdap($values): ?array {
A::ensure_narray($values);
if ($values !== null) {
foreach ($values as &$value) {
$value = $this->ldap2php($value);
}; unset($value);
}
return cl::filter_n($values)?: null;
}
/** transformer la valeur d'un attribut LDAP en PHP */
function fromMonovaluedLdap($value) {
if (is_array($value)) $value = cl::first($value);
if ($value === null) return null;
else return $this->ldap2php($value);
}
/** transformer une(des) valeur(s) PHP en attribut LDAP */
function fromPhp($values): ?array {
A::ensure_narray($values);
if ($values !== null) {
foreach ($values as &$value) {
$value = $this->php2ldap($value);
}; unset($value);
}
return cl::filter_n($values)?: null;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\ldap\syntaxes;
class BinarySyntax extends AbstractSyntax {
function php2ldap($value): ?string {
throw IllegalAccessException::not_implemented();
}
function ldap2php(string $value) {
throw IllegalAccessException::not_implemented();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\ldap\syntaxes;
class BooleanSyntax extends AbstractSyntax {
function getPhpType(): ?string {
return "bool";
}
function php2ldap($value): ?string {
if ($value === null) return null;
else return $value? "TRUE": "FALSE";
}
function fromPhp($values): ?array {
if (is_bool($values)) $values = [$values];
return parent::fromPhp($values);
}
function ldap2php(string $value): bool {
return $value === "TRUE";
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace nulib\ldap\syntaxes;
use nulib\A;
use nulib\cl;
use nulib\ldap\CompositeAttr;
use nulib\ldap\CompositeValue;
class CompositeSyntax extends AbstractSyntax {
/**
* @var string la classe dérivée de {@link CompositeAttr} qui porte l'attribut
*/
const CACLASS = CompositeAttr::class;
function getAttrClass(): string {
return static::CACLASS;
}
function newAttr(string $name, ?array &$values, ?int $flags): CompositeAttr {
$attrClass = $this->getAttrClass();
return new $attrClass($name, $values, $this, $flags);
}
/**
* @var string la classe dérivée de {@link CompositeValue} qui porte les
* valeurs de cette syntaxe
*/
const CVCLASS = CompositeValue::class;
/** retourner la classe d'une valeur composite */
function getPhpType(): ?string {
return static::CVCLASS;
}
protected function newCompositeValue(): CompositeValue {
$class = $this->getPhpType();
/** @var CompositeValue $cvalue */
$cvalue = new $class;
return $cvalue->setup($this->conn);
}
function ensureArray($values): ?array {
A::ensure_narray($values);
if ($values === null) return null;
# déterminer si $values est *une* valeur ou une liste de valeurs
$list = false;
foreach ($values as $value) {
if (is_array($value) || $value instanceof CompositeValue) {
$list = true;
break;
}
}
if (!$list) $values = [$values];
return $values;
}
function ensureComposite($value): ?CompositeValue {
if ($value === null) return null;
if (is_array($value)) {
$value = $this->newCompositeValue()->reset($value);
}
ValueException::check_class($value, $this->getPhpType());
return $value;
}
/** @param ?CompositeValue $value */
function php2ldap($value): ?string {
$cvalue = $this->ensureComposite($value);
if ($cvalue === null) return null;
else return $cvalue->formatLdap();
}
function ldap2php(string $value): CompositeValue {
return $this->newCompositeValue()->parseLdap($value);
}
function fromMultivaluedLdap($values): ?array {
A::ensure_narray($values);
if ($values !== null) {
$tmp = [];
foreach ($values as $value) {
$value = $this->ldap2php($value);
$key = $value->getKey();
$tmp[$key] = $value;
}
$values = $tmp;
}
return cl::filter_n($values)?: null;
}
function fromPhp($values): ?array {
$values = $this->ensureArray($values);
return parent::fromPhp($values);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace nulib\ldap\syntaxes;
use DateTimeImmutable;
class DateSyntax extends AbstractSyntax {
function __construct() {
$this->type = new SDatetimeType();
}
/** @var SDatetimeType */
protected $type;
function php2ldap($value): ?string {
$value = $this->type->with($value);
if ($value === null) return null;
$datetime = new Datetime($value);
return $datetime->formatRfc4517();
}
function ldap2php(string $value) {
$d = DateTimeImmutable::createFromFormat("YmdHisT", $value);
$value = $d->format("d/m/Y H:i:s");
$value = preg_replace('/ 00:00:00$/', "", $value);
return $value;
/*
[$y, $m, $d, $H, $M, $S] = [
substr($value, 0, 4),
substr($value, 4, 2),
substr($value, 6, 2),
substr($value, 8, 2),
substr($value, 10, 2),
substr($value, 12, 2),
];
$datetime = new Datetime(gmmktime($H, $M, $S, $m, $d, $y));
$value = preg_replace('/ 00:00:00$/', "", $datetime->format());
return $value;
*/
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace nulib\ldap\syntaxes;
class IntegerSyntax extends AbstractSyntax {
function getPhpType(): ?string {
return "int";
}
function php2ldap($value): ?string {
if ($value === null) return null;
else return strval($value);
}
function ldap2php(string $value): int {
return intval($value);
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace nulib\ldap\syntaxes;
class MailSyntax extends StringSyntax {
}

View File

@ -0,0 +1,20 @@
<?php
namespace nulib\ldap\syntaxes;
class PostalAddressSyntax extends StringSyntax {
function php2ldap($value): ?string {
$value = parent::php2ldap($value);
if ($value === null) return null;
// mettre en échappement tout caractère $
$value = str_replace('$', '\$', $value);
$value = preg_replace('/\r?\n/', '$', $value);
$value = preg_replace('/\s*(?<!\\\\)\$\s*/', ' $ ', $value);
return $value;
}
function ldap2php(string $value): string {
$value = preg_replace('/\s*(?<!\\\\)\$\s*/', "\n", $value);
$value = preg_replace('/\\\\\$/', '$', $value);
return $value;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace nulib\ldap\syntaxes;
class PrintableSyntax extends StringSyntax {
const DISALLOWED = '/[^a-zA-Z0-9"()+,-.\/:? -]+/';
/** enlever les caractères interdit de la chaine */
function filter(?string $value): ?string {
if ($value === null) return null;
return preg_replace(self::DISALLOWED, "", $value);
}
function php2ldap($value): ?string {
$value = parent::php2ldap($value);
if (preg_match(self::DISALLOWED, $value)) {
throw new SyntaxException("invalid string: $value");
}
return $value;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\ldap\syntaxes;
class StringSyntax extends AbstractSyntax {
function php2ldap($value): ?string {
if ($value === null) return null;
else return trim(strval($value));
}
function ldap2php(string $value): string {
return $value;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace nulib\ldap\syntaxes;
use nulib\ValueException;
/**
* Class SyntaxException: indique qu'une valeur PHP ne peut être convertie en
* valeur LDAP
*/
class SyntaxException extends ValueException {
}

View File

@ -0,0 +1,28 @@
<?php
namespace nulib\ldap\syntaxes;
use nulib\ldap\labels;
class TelephoneSyntax extends StringSyntax {
function __construct() {
$this->type = new TelephoneType();
}
/** @var TelephoneType */
protected $type;
function php2ldap($value): ?string {
$value = parent::php2ldap($value);
if ($value === null) return null;
labels::strip($value, $label);
$type = $this->type;
$value = $type->ensureInternational($type->with($value));
return labels::add($value, $label);
}
function ldap2php(string $value): string {
labels::strip($value, $label);
$value = $this->type->ensureLocal($value);
return labels::add($value, $label);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace nulib\ldap\syntaxes;
class cvalues {
static function autogen_properties(array $schema): array {
$md = Metadata::with($schema);
$properties = [];
foreach ($md->getKeys() as $key) {
$type = $md->getType($key);
[$phpType, $returnType] = Autogen::fix_type($type->getPhpType());
$properties[] = "$returnType \$$key";
}
return $properties;
}
}