diff --git a/CHANGES.md b/CHANGES.md index 9b9a127..e213929 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,6 @@ -## Release 0.7.0p82 du 22/10/2025-19:08 +## Release 0.8.0p74 du 07/11/2025-10:39 + +* `65fbe88` début migration ldap ## Release 0.7.0p74 du 22/10/2025-19:06 diff --git a/VERSION.txt b/VERSION.txt index faef31a..a3df0a6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.7.0 +0.8.0 diff --git a/cli/LdapApplication.php b/cli/LdapApplication.php new file mode 100644 index 0000000..fc31743 --- /dev/null +++ b/cli/LdapApplication.php @@ -0,0 +1,25 @@ + "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, + ], + ]; +} diff --git a/cli/LdapDeleteApp.php b/cli/LdapDeleteApp.php new file mode 100644 index 0000000..7ad6fd0 --- /dev/null +++ b/cli/LdapDeleteApp.php @@ -0,0 +1,35 @@ + 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(); + } + } +} diff --git a/cli/LdapGetInfosApp.php b/cli/LdapGetInfosApp.php new file mode 100644 index 0000000..44e9324 --- /dev/null +++ b/cli/LdapGetInfosApp.php @@ -0,0 +1,29 @@ + 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); + } +} diff --git a/cli/LdapSearchApp.php b/cli/LdapSearchApp.php new file mode 100644 index 0000000..08cbf06 --- /dev/null +++ b/cli/LdapSearchApp.php @@ -0,0 +1,62 @@ + 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(); + } +} diff --git a/cli/TLdapApplication.php b/cli/TLdapApplication.php new file mode 100644 index 0000000..242e790 --- /dev/null +++ b/cli/TLdapApplication.php @@ -0,0 +1,34 @@ +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); + } +} diff --git a/composer.lock b/composer.lock index 94a6934..13ae371 100644 --- a/composer.lock +++ b/composer.lock @@ -1001,7 +1001,7 @@ "dist": { "type": "path", "url": "../nulib-base", - "reference": "b6d53059a3dc39d0aa9d4492c362e9f1d1cce16e" + "reference": "aa7809124ae7ba3f6c3a04e410210d6949896bdd" }, "require": { "ext-json": "*", @@ -1030,9 +1030,13 @@ "php/bin/dumpser.php", "php/bin/json2yml.php", "php/bin/yml2json.php", + "php/bin/db.dump.php", "php/bin/sqlite.capacitor.php", + "php/bin/sqlite.load.php", + "php/bin/sqlite.dump.php", "php/bin/mysql.capacitor.php", - "php/bin/pgsql.capacitor.php" + "php/bin/pgsql.capacitor.php", + "php/bin/create-capacitor-channels.php" ], "type": "library", "extra": { @@ -1069,7 +1073,7 @@ "dist": { "type": "path", "url": "../nulib-phpss", - "reference": "06802fe052af5259cd4a1d83d928025ed69bbefa" + "reference": "432fe5305a9e5a4b63acf1c97f5d4bad65a8ac2c" }, "require": { "nulib/base": "^7.4-dev", @@ -1113,7 +1117,7 @@ "dist": { "type": "path", "url": "../nulib-spout", - "reference": "d843ef4600a6906debc9b6947971f7fe58e4869c" + "reference": "16324d66f6b17e5ddce96cfa2d90c246d9d54ef8" }, "require": { "ext-dom": "*", @@ -1402,16 +1406,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.30.0", + "version": "1.30.1", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "2f39286e0136673778b7a142b3f0d141e43d1714" + "reference": "fa8257a579ec623473eabfe49731de5967306c4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2f39286e0136673778b7a142b3f0d141e43d1714", - "reference": "2f39286e0136673778b7a142b3f0d141e43d1714", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c", "shasum": "" }, "require": { @@ -1433,7 +1437,7 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^7.4 || ^8.0", + "php": ">=7.4.0 <8.5.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" @@ -1502,9 +1506,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" }, - "time": "2025-08-10T06:28:02+00:00" + "time": "2025-10-26T16:01:04+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/ldap/CompositeAttr.php b/src/ldap/CompositeAttr.php new file mode 100644 index 0000000..522c01f --- /dev/null +++ b/src/ldap/CompositeAttr.php @@ -0,0 +1,61 @@ +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"); + } +} diff --git a/src/ldap/CompositeValue.php b/src/ldap/CompositeValue.php new file mode 100644 index 0000000..6199405 --- /dev/null +++ b/src/ldap/CompositeValue.php @@ -0,0 +1,153 @@ + $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"]]; +} diff --git a/src/ldap/ILdapWalker.php b/src/ldap/ILdapWalker.php new file mode 100644 index 0000000..2776d11 --- /dev/null +++ b/src/ldap/ILdapWalker.php @@ -0,0 +1,9 @@ +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); } +} diff --git a/src/ldap/LdapConn.php b/src/ldap/LdapConn.php new file mode 100644 index 0000000..45652ea --- /dev/null +++ b/src/ldap/LdapConn.php @@ -0,0 +1,434 @@ + ["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"); + } + } +} diff --git a/src/ldap/LdapException.php b/src/ldap/LdapException.php new file mode 100644 index 0000000..da1f0f0 --- /dev/null +++ b/src/ldap/LdapException.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/src/ldap/LdapObject.php b/src/ldap/LdapObject.php new file mode 100644 index 0000000..b6de2f2 --- /dev/null +++ b/src/ldap/LdapObject.php @@ -0,0 +1,372 @@ +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"]]; +} diff --git a/src/ldap/LdapSearch.php b/src/ldap/LdapSearch.php new file mode 100644 index 0000000..198916f --- /dev/null +++ b/src/ldap/LdapSearch.php @@ -0,0 +1,215 @@ + ["?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]; + } +} diff --git a/src/ldap/LdapWalker.php b/src/ldap/LdapWalker.php new file mode 100644 index 0000000..d1d1f19 --- /dev/null +++ b/src/ldap/LdapWalker.php @@ -0,0 +1,10 @@ +md()->ensureSchema($values); + $this->data = $values; + return $this; + } +} diff --git a/src/ldap/TLdapWalker.php b/src/ldap/TLdapWalker.php new file mode 100644 index 0000000..fc656f4 --- /dev/null +++ b/src/ldap/TLdapWalker.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/src/ldap/consts.php b/src/ldap/consts.php new file mode 100644 index 0000000..74e7a4a --- /dev/null +++ b/src/ldap/consts.php @@ -0,0 +1,315 @@ + [ + '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\""], + ]; +} diff --git a/src/ldap/filters.php b/src/ldap/filters.php new file mode 100644 index 0000000..b70cc1f --- /dev/null +++ b/src/ldap/filters.php @@ -0,0 +1,96 @@ + $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); + } +} diff --git a/src/ldap/io/LdapWriter.php b/src/ldap/io/LdapWriter.php new file mode 100644 index 0000000..6cab047 --- /dev/null +++ b/src/ldap/io/LdapWriter.php @@ -0,0 +1,25 @@ +write($object, $names); + $writer->close(); + } + + function __construct($output=null) { + $this->writer = file::writer($output); + } + + /** @var IWriter */ + protected $writer; + + function close(): void { + $this->writer->close(); + } +} diff --git a/src/ldap/io/LdifWriter.php b/src/ldap/io/LdifWriter.php new file mode 100644 index 0000000..113e44c --- /dev/null +++ b/src/ldap/io/LdifWriter.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/src/ldap/io/YamlWriter.php b/src/ldap/io/YamlWriter.php new file mode 100644 index 0000000..5deeed9 --- /dev/null +++ b/src/ldap/io/YamlWriter.php @@ -0,0 +1,30 @@ +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; + } +} diff --git a/src/ldap/labels.php b/src/ldap/labels.php new file mode 100644 index 0000000..f139b11 --- /dev/null +++ b/src/ldap/labels.php @@ -0,0 +1,22 @@ + ["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); + } +} diff --git a/src/ldap/ldap_config.php b/src/ldap/ldap_config.php new file mode 100644 index 0000000..c054ebe --- /dev/null +++ b/src/ldap/ldap_config.php @@ -0,0 +1,31 @@ + 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); + } +} diff --git a/src/ldap/scheman.php b/src/ldap/scheman.php new file mode 100644 index 0000000..0a1a355 --- /dev/null +++ b/src/ldap/scheman.php @@ -0,0 +1,30 @@ +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); + } +} diff --git a/src/ldap/schemas/LdapSchemaExtractor.php b/src/ldap/schemas/LdapSchemaExtractor.php new file mode 100644 index 0000000..a0ee223 --- /dev/null +++ b/src/ldap/schemas/LdapSchemaExtractor.php @@ -0,0 +1,245 @@ + $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; + } +} diff --git a/src/ldap/schemas/LseAttribute.php b/src/ldap/schemas/LseAttribute.php new file mode 100644 index 0000000..7dd10a2 --- /dev/null +++ b/src/ldap/schemas/LseAttribute.php @@ -0,0 +1,84 @@ +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; + } +} diff --git a/src/ldap/schemas/LseObjectClass.php b/src/ldap/schemas/LseObjectClass.php new file mode 100644 index 0000000..34c0856 --- /dev/null +++ b/src/ldap/schemas/LseObjectClass.php @@ -0,0 +1,61 @@ +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; + } +} diff --git a/src/ldap/schemas/LseParser.php b/src/ldap/schemas/LseParser.php new file mode 100644 index 0000000..e1e99b5 --- /dev/null +++ b/src/ldap/schemas/LseParser.php @@ -0,0 +1,120 @@ +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 = $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(""); + } + $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; + } +} diff --git a/src/ldap/schemas/LseSyntax.php b/src/ldap/schemas/LseSyntax.php new file mode 100644 index 0000000..2502f81 --- /dev/null +++ b/src/ldap/schemas/LseSyntax.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/src/ldap/schemas/SchemaManager.php b/src/ldap/schemas/SchemaManager.php new file mode 100644 index 0000000..05acb96 --- /dev/null +++ b/src/ldap/schemas/SchemaManager.php @@ -0,0 +1,79 @@ +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; + } +} diff --git a/src/ldap/syntaxes/AbstractSyntax.php b/src/ldap/syntaxes/AbstractSyntax.php new file mode 100644 index 0000000..7cef645 --- /dev/null +++ b/src/ldap/syntaxes/AbstractSyntax.php @@ -0,0 +1,59 @@ +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; + } +} diff --git a/src/ldap/syntaxes/BinarySyntax.php b/src/ldap/syntaxes/BinarySyntax.php new file mode 100644 index 0000000..8a2ddc2 --- /dev/null +++ b/src/ldap/syntaxes/BinarySyntax.php @@ -0,0 +1,13 @@ +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); + } +} diff --git a/src/ldap/syntaxes/DateSyntax.php b/src/ldap/syntaxes/DateSyntax.php new file mode 100644 index 0000000..85598a8 --- /dev/null +++ b/src/ldap/syntaxes/DateSyntax.php @@ -0,0 +1,41 @@ +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; + */ + } +} diff --git a/src/ldap/syntaxes/IntegerSyntax.php b/src/ldap/syntaxes/IntegerSyntax.php new file mode 100644 index 0000000..5ab09b5 --- /dev/null +++ b/src/ldap/syntaxes/IntegerSyntax.php @@ -0,0 +1,17 @@ +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); + } +} diff --git a/src/ldap/syntaxes/cvalues.php b/src/ldap/syntaxes/cvalues.php new file mode 100644 index 0000000..bd0a211 --- /dev/null +++ b/src/ldap/syntaxes/cvalues.php @@ -0,0 +1,16 @@ +getKeys() as $key) { + $type = $md->getType($key); + [$phpType, $returnType] = Autogen::fix_type($type->getPhpType()); + $properties[] = "$returnType \$$key"; + } + return $properties; + } +} diff --git a/src/web/content/README.md b/src/web/content/README.md index 7d1bf6b..1390bfa 100644 --- a/src/web/content/README.md +++ b/src/web/content/README.md @@ -37,7 +37,7 @@ $content2 = [ "class" => "first second", "attr" => "static true", "before", - ...cl::with(func("arg")), + ...cl::with(func($arg)), "after", ]; ~~~