Commit 25bb232b authored by Côme Chilliet's avatar Côme Chilliet
Browse files

Merge branch '1-import-library-from-schema2ldif' into 'master'

Resolve "Import library from schema2ldif"

See merge request !1
parents ecda84f8 19b9339d
Pipeline #8922 failed with stages
in 1 minute and 16 seconds
......@@ -24,12 +24,11 @@ create_php_lint_rapport_stretch:
# PHP codesniffer
create_php_code_sniffer_rapport:
image: phpcodesniffer-cli:stretch
image: registry.fusiondirectory.org/fusiondirectory/fd/phpcodesniffer-cli:stretch
stage: codestyle
only:
- branches
script:
- git clone --depth 1 https://gitlab.fusiondirectory.org/fusiondirectory/dev-tools.git ../dev-tools
- find . -type f -name '*.php' -o -name '*.inc' > ./filelist
- phpcs --ignore=class_sieve.inc --standard=../dev-tools/php-codesniffer-rules/FDStandard/ruleset.xml --file-list=./filelist
- phpcs --standard=../dev-tools/php-codesniffer-rules/FDStandard/ruleset.xml --file-list=./filelist
# Projet de plugin
# FusionDirectory LDAP library
projet de base pour l'ecriture d'un plugin client
Modern Object Oriented PHP LDAP library which is used by FusionDirectory cli tools.
Usage:
```php
<?php
require 'FusionDirectory/Ldap/autoload.php';
use FusionDirectory\Ldap;
$ldap = new Ldap\Link('ldapi:///');
/* Only EXTERNAL bind is available for now */
$ldap->bind();
/* Make a search */
$list = $ldap->search('ou=people,dc=example,dc=com', '(cn=*)', ['cn'], 'one');
/* Throw FusionDirectory\Ldap\Exception if there was an error */
$list->assert();
/* Browse results, Ldap\Result is Traversable */
foreach ($list as $dn => $attributes) {
echo $dn.': '.$attributes['cn'][0]."\n";
}
/* Ldap\Result is also Countable */
echo 'There was '.count($list).' results'."\n";
```
{
"name": "fusiondirectory/ldap",
"type": "library",
"license": "GPL2+",
"authors": [
{
"name": "Côme Chilliet",
"email": "come.chilliet@fusiondirectory.org"
}
],
"require": {},
"autoload": {
"psr-4": {"FusionDirectory\\Ldap\\": "src/FusionDirectory/Ldap"}
}
}
<?php
/*
This code is part of FusionDirectory\Ldap (https://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
declare(strict_types = 1);
namespace FusionDirectory\Ldap;
class Acl
{
/**
* @var int
*/
protected $index;
/**
* @var array<string,string>|string
*/
protected $to;
/**
* @var array<int,array>
*/
protected $by = [];
/*
<access directive> ::= to <what> [by <who> [<access>] [<control>]]+
<what> ::= * | [dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>] [filter=<ldapfilter>] [attrs=<attrlist>]
<basic-style> ::= regex | exact
<scope-style> ::= base | one | subtree | children
<attrlist> ::= <attr> [val[.<basic-style>]=<regex>] | <attr> , <attrlist>
<attr> ::= <attrname> | entry | children
<who> ::= * | [anonymous | users | self
| dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
[dnattr=<attrname>]
[group[/<objectclass>[/<attrname>][.<basic-style>]]=<regex>]
[peername[.<basic-style>]=<regex>]
[sockname[.<basic-style>]=<regex>]
[domain[.<basic-style>]=<regex>]
[sockurl[.<basic-style>]=<regex>]
[set=<setspec>]
[aci=<attrname>]
<access> ::= [self]{<level>|<priv>}
<level> ::= none | disclose | auth | compare | search | read | write | manage
<priv> ::= {=|+|-}{m|w|r|s|c|x|d|0}+
<control> ::= [stop | continue | break]
*/
public function __construct (string $acl)
{
if (preg_match('/^{(\d+)}/', $acl, $m)) {
$this->index = (int)($m[1]);
$acl = substr($acl, strlen($m[0]));
}
$tokens = preg_split('/\s/', $acl);
if (($tokens === FALSE) || ($tokens[0] != 'to')) {
throw new \Exception('Invalid ACL format: missing "to" keyword');
}
$this->parseTo($tokens, 1);
}
/**
* @param array<string> $tokens
*/
protected function parseTo (array $tokens, int $i): void
{
/*
<what> ::= * | [dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>] [filter=<ldapfilter>] [attrs=<attrlist>]
<basic-style> ::= regex | exact
<scope-style> ::= base | one | subtree | children
*/
if ($tokens[$i] == '*') {
$this->to = '*';
$i++;
} else {
$this->to = [];
do {
[$key, $value] = explode('=', $tokens[$i], 2);
switch ($key) {
case 'filter':
case 'attrs':
case 'dn':
case 'dn.regex':
case 'dn.exact':
case 'dn.base':
case 'dn.one':
case 'dn.subtree':
case 'dn.children':
$this->to[$key] = $value;
break;
default:
throw new Exception('Could not parse ACL: invalid "to" clause: "'.$tokens[$i].'"');
}
$i++;
} while (($i < count($tokens)) && ($tokens[$i] != 'by'));
}
if (($i < count($tokens)) && ($tokens[$i] == 'by')) {
$this->parseBy($tokens, $i + 1);
} else {
throw new Exception('Could not parse ACL: Missing "by" clause');
}
}
/**
* @param array<string> $tokens
*/
protected function parseBy (array $tokens, int $i): void
{
/* [by <who> [<access>] [<control>]]+ */
$by = [];
do {
$by[] = $tokens[$i];
$i++;
} while (($i < count($tokens)) && ($tokens[$i] != 'by'));
$this->by[] = $by;
if ($i < count($tokens)) {
if ($tokens[$i] == 'by') {
$this->parseBy($tokens, $i + 1);
} else {
throw new Exception('Could not parse ACL: Invalid clause: "'.$tokens[$i].'"');
}
}
}
public function dump (string $indent): void
{
echo $indent.$this->index.': to ';
if (is_array($this->to)) {
foreach ($this->to as $key => $attr) {
echo $key.'='.$attr.' ';
}
} else {
echo $this->to;
}
echo "\n";
foreach ($this->by as $by) {
echo $indent.' by '.implode(' ', $by)."\n";
}
}
}
<?php
/*
This code is part of FusionDirectory\Ldap (https://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
declare(strict_types = 1);
namespace FusionDirectory\Ldap;
class Exception extends \Exception
{
}
<?php
/*
This code is part of FusionDirectory\Ldap (https://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*! \class LdapGeneralizedTime
\brief LdapGeneralizedTime allows you to convert from and to LDAP GeneralizedTime format PHP DateTime objects
This class provides function to convert from LDAP GeneralizedTime to DateTime and the other way.
Please note that leap seconds will be lost as PHP has no support for it (see https://bugs.php.net/bug.php?id=70335). 01:60 will become 02:00.
Also, this class does not support fraction of hours or fraction of minutes (fraction of seconds are supported).
*/
declare(strict_types = 1);
namespace FusionDirectory\Ldap;
use DateTime, DateTimeZone;
class GeneralizedTime
{
/**
* @brief Convert from LDAP GeneralizedTime formatted string to DateTime object
* @param string $string GeneralizedTime formatted string to convert
*/
public static function fromString (string $string): DateTime
{
// century = 2(%x30-39) ; "00" to "99"
// year = 2(%x30-39) ; "00" to "99"
$year = '(?P<year>\d{4})';
// month = ( %x30 %x31-39 ) ; "01" (January) to "09"
// / ( %x31 %x30-32 ) ; "10" to "12"
$month = '(?P<month>0[1-9]|1[0-2])';
// day = ( %x30 %x31-39 ) ; "01" to "09"
// / ( %x31-32 %x30-39 ) ; "10" to "29"
// / ( %x33 %x30-31 ) ; "30" to "31"
$day = '(?P<day>0[1-9]|[0-2]\d|3[01])';
// hour = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
$hour = '(?P<hour>[0-1]\d|2[0-3])';
// minute = %x30-35 %x30-39 ; "00" to "59"
$minute = '(?P<minute>[0-5]\d)';
// second = ( %x30-35 %x30-39 ) ; "00" to "59"
// leap-second = ( %x36 %x30 ) ; "60"
$second = '(?P<second>[0-5]\d|60)';
// fraction = ( DOT / COMMA ) 1*(%x30-39)
$fraction = '([.,](?P<fraction>\d+))';
// g-time-zone = %x5A ; "Z"
// / g-differential
// g-differential = ( MINUS / PLUS ) hour [ minute ]
$timezone = '(?P<timezone>Z|[-+]([0-1]\d|2[0-3])([0-5]\d)?)';
// GeneralizedTime = century year month day hour
// [ minute [ second / leap-second ] ]
// [ fraction ]
// g-time-zone
$pattern = '/^'.
"$year$month$day$hour".
"($minute$second?)?".
"$fraction?".
$timezone.
'$/';
if (preg_match($pattern, $string, $m)) {
if (empty($m['minute'])) {
$m['minute'] = '00';
}
if (empty($m['second'])) {
$m['second'] = '00';
}
if (empty($m['fraction'])) {
$m['fraction'] = '0';
}
$date = new DateTime($m['year'].'-'.$m['month'].'-'.$m['day'].'T'.$m['hour'].':'.$m['minute'].':'.$m['second'].'.'.$m['fraction'].$m['timezone']);
$date->setTimezone(new DateTimeZone('UTC'));
return $date;
} else {
throw new Exception("$string does not match LDAP GeneralizedTime format");
}
}
/**
* @brief Convert from DateTime object to LDAP GeneralizedTime formatted string
* @param DateTime $date DateTime object to convert
* @param boolean $setToUTC Whether or not to set the date timezone to UTC. Defaults to TRUE.
*/
public static function toString (DateTime $date, bool $setToUTC = TRUE): string
{
if ($setToUTC) {
$date->setTimezone(new DateTimeZone('UTC'));
}
$fraction = preg_replace('/0+$/', '', $date->format('u'));
$string = $date->format('YmdHis');
if (empty($fraction)) {
return preg_replace('/(00){1,2}$/', '', $string).'Z';
} else {
return $string.'.'.$fraction.'Z';
}
}
}
<?php
/*
This code is part of FusionDirectory\Ldap (https://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
declare(strict_types = 1);
namespace FusionDirectory\Ldap;
class Link
{
/**
* @var resource
*/
protected $cid;
/**
* @var string
*/
protected $hostname;
/**
* @var bool
*/
protected $tls;
public function __construct (string $hostname, bool $tls = FALSE)
{
$this->hostname = $hostname;
$this->tls = $tls;
}
public function bind (): void
{
$cid = ldap_connect($this->hostname);
if (!$cid) {
throw new Exception('Invalid URI: '.$this->hostname);
}
ldap_set_option($cid, LDAP_OPT_PROTOCOL_VERSION, 3);
if ($this->tls) {
ldap_start_tls($cid);
}
if (ldap_sasl_bind($cid, '', '', 'EXTERNAL') !== TRUE) {
throw new Exception('Failed to bind to '.$this->hostname);
}
$this->cid = $cid;
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function search (string $basedn, string $filter, array $attrs = [], string $scope = 'subtree', array $controls = NULL): Result
{
$functions = ['base' => 'ldap_read','one' => 'ldap_list','subtree' => 'ldap_search'];
if (isset($controls)) {
/* @phpstan-ignore-next-line
* phpstan has an outdated signature for php-ldap functions */
$result = @$functions[strtolower($scope)]($this->cid, $basedn, $filter, $attrs, 0, 0, 0, LDAP_DEREF_NEVER, $controls);
} else {
$result = @$functions[strtolower($scope)]($this->cid, $basedn, $filter, $attrs);
}
if ($result === FALSE) {
throw new Exception('Search failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_add (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_add_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod add failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_replace (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_replace_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod replace failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_del (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_del_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod del failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<array> $controls
*/
public function delete (string $dn, array $controls = []): Result
{
$result = ldap_delete_ext($this->cid, $dn, $controls);
if ($result === FALSE) {
throw new Exception('Delete failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @return array<string,array<string,string|true|array<string>>>
*/
public function getObjectClasses (): array
{
// Get base to look for schema
$res = $this->search('', 'objectClass=*', ['subschemaSubentry'], 'base');
$objectclasses = [];
foreach ($res as $attrs) {
if (!isset($attrs['subschemaSubentry'][0])) {
continue;
}
/* Get list of objectclasses and fill array */
$nb = $attrs['subschemaSubentry'][0];
$res2 = $this->search($nb, 'objectClass=*', ['objectClasses'], 'base');
foreach ($res2 as $attrs2) {
foreach ($attrs2['objectClasses'] as $val) {
$infos = static::parseObjectClassDefinition($val);
if (isset($infos['NAME']) && is_string($infos['NAME'])) {
$objectclasses[$infos['NAME']] = $infos;
} else {
throw new Exception('Invalid NAME in class definition: '.print_r($infos['NAME'] ?? NULL, TRUE));
}
}
}
}
return $objectclasses;
}
/**
* @return array<string,string|true|array<string>>
*/
public static function parseObjectClassDefinition (string $definition): array
{
$name = 'OID';
$value = '';
$pattern = explode(' ', $definition);
$infos = [];
foreach ($pattern as $chunk) {
switch ($chunk) {
case '(':
$value = '';
break;
case ')':
$chunk = '';
case 'NAME':
case 'DESC':
case 'SUP':
case 'STRUCTURAL':
case 'ABSTRACT':
case 'AUXILIARY':
case 'MUST':
case 'MAY':
if ($name != '') {
$v = static::value2container($value);
if (in_array($name, ['MUST','MAY'])) {
if ($v === TRUE) {
$v = [];
} else if (!is_array($v)) {
$v = [$v];
}
}
$infos[$name] = $v;
}
$name = $chunk;
$value = '';
break;
default: $value .= $chunk.' ';
}
}
return $infos;
}
/**
* @return string|true|array<string>
*/
protected static function value2container (string $value)
{
/* Set empty values to "TRUE" only */
if (preg_match('/^\s*$/', $value)) {
return TRUE;
}
/* Remove ' and " if needed */
$value = (preg_replace('/^[\'"]/', '', $value) ?? '');
$value = (preg_replace('/[\'"] *$/', '', $value) ?? '');
$value = rtrim($value);