Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Userland protocol impl #2

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a84d86b
Initial release: Protocol / stream implementation
thekid May 6, 2014
9c3e84d
Utilize new BufferedInputStream::pushBack()
thekid May 6, 2014
8e18258
Add badges: XP, BSD licence, PHP 5.4+
thekid May 6, 2014
66e25e6
Merge branch 'feature/userland-protocol-impl' of github.com:xp-framew…
thekid May 6, 2014
07cebb0
First working reader
thekid May 6, 2014
cf34029
Move handling of multiple results into send()
thekid May 6, 2014
e584c3a
Make in and out members protected
thekid May 6, 2014
84ccf07
Refactor: REP -> RES (better abbreviation for response)
thekid May 6, 2014
2cb998a
Make base passable to search() as parameter
thekid May 6, 2014
cd08bb2
Fill in enums
thekid May 7, 2014
dcf9de0
QA: Add api documentation to all methods
thekid May 8, 2014
bc59946
Use larger default buffer for reading
thekid May 8, 2014
a636697
Implement bind operation
thekid May 8, 2014
fab460b
Document bind() returns void, throws an LDAPException
thekid May 8, 2014
751ee12
QA: WS
thekid May 8, 2014
1d2bb2e
Throw exceptions if search failed
thekid May 8, 2014
4207884
Add explicit close() method
thekid May 8, 2014
4370bf0
Refactor: Extract response handling into helper method
thekid May 8, 2014
78722aa
MFH
thekid Jan 16, 2015
48d7ced
Merge branch 'master' into feature/userland-protocol-impl
thekid Jul 30, 2018
c5391a6
Remove lang.Object base class
thekid Jul 30, 2018
c507e1e
Merge branch 'master' into feature/userland-protocol-impl
thekid Jul 30, 2018
518e090
Extract search(), connect(), bind() and close() to library class
thekid Aug 16, 2018
a2c60ca
Implement substring filter, hardcoded at the moment
thekid Aug 16, 2018
ec35815
Add parser for LDAP filters
thekid Sep 9, 2018
bbf6dbc
Add tests for invalid LDAP filter syntax
thekid Sep 9, 2018
98419f4
Fix PHP 5.6 compatibility
thekid Sep 9, 2018
4bfd8aa
Rewrite all() to return an array
thekid Sep 9, 2018
c8945e5
Add public "kind" member
thekid Sep 9, 2018
a0d6abb
Use filters API
thekid Sep 9, 2018
b599a3c
Implement writing filters
thekid Sep 9, 2018
3b18359
Add peek()
thekid Sep 9, 2018
c4a7167
Implement RES_EXTENSION
thekid Sep 9, 2018
b080bde
Fix size
thekid Sep 9, 2018
9416b66
Extract wrapping in LDAPSearchResult to LDAPConnection class
thekid Sep 10, 2018
6278ac6
QA: Apidocs, whitespace
thekid Sep 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 63 additions & 116 deletions src/main/php/peer/ldap/LDAPConnection.class.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?php namespace peer\ldap;

use lang\IllegalArgumentException;
use lang\Value;
use lang\XPClass;
use peer\ConnectException;
use peer\URL;
use lang\XPClass;
use lang\IllegalArgumentException;
use peer\ldap\util\LdapLibrary;
use peer\ldap\util\LdapProtocol;
use util\Secret;

/**
Expand All @@ -23,31 +26,12 @@
* @ext ldap
* @test xp://peer.ldap.unittest.LDAPConnectionTest
*/
class LDAPConnection {
private static $options;

class LDAPConnection implements Value {
private $url;
private $handle= null;

static function __static() {
XPClass::forName('peer.ldap.LDAPException'); // Error codes
self::$options= [
'deref' => function($handle, $value) {
return ldap_set_option($handle, LDAP_OPT_DEREF, constant('LDAP_DEREF_'.strtoupper($value)));
},
'sizelimit' => function($handle, $value) {
return ldap_set_option($handle, LDAP_OPT_SIZELIMIT, (int)$value);
},
'timelimit' => function($handle, $value) {
return ldap_set_option($handle, LDAP_OPT_TIMELIMIT, (int)$value);
},
'network_timeout' => function($handle, $value) {
return ldap_set_option($handle, LDAP_OPT_NETWORK_TIMEOUT, (int)$value);
},
'protocol_version' => function($handle, $value) {
return ldap_set_option($handle, LDAP_OPT_PROTOCOL_VERSION, (int)$value);
},
];
}

/**
Expand All @@ -58,12 +42,18 @@ static function __static() {
* @throws lang.IllegalArgumentException when DSN is malformed
*/
public function __construct($dsn) {
static $ports= ['ldap' => 389, 'ldaps' => 636];

// TODO: Driver!
$impl= getenv('PROTO') ? LdapProtocol::class : LdapLibrary::class;

$this->url= $dsn instanceof URL ? $dsn : new URL($dsn);
foreach ($this->url->getParams() as $option => $value) {
if (!isset(self::$options[$option])) {
throw new IllegalArgumentException('Unknown option "'.$option.'"');
}
}
$this->proto= new $impl(
$this->url->getScheme(),
$this->url->getHost(),
$this->url->getPort($ports[$this->url->getScheme()]),
$this->url->getParams()
);
}

/** @return peer.URL */
Expand All @@ -79,45 +69,12 @@ public function dsn() { return $this->url; }
* @throws peer.ConnectException
*/
public function connect($dn= null, Secret $password= null) {
static $ports= ['ldap' => 389, 'ldaps' => 636];

if ($this->isConnected()) return true;
if ($this->proto->connected()) return true;

$uri= sprintf(
'%s://%s:%d',
$this->url->getScheme(),
$this->url->getHost(),
$this->url->getPort($ports[$this->url->getScheme()])
$this->proto->connect(
$dn ?: $this->url->getUser(null),
$password ?: new Secret($this->url->getPassword(null))
);
if (false === ($this->handle= ldap_connect($uri))) {
throw new ConnectException('Cannot connect to '.$uri);
}

foreach (array_merge(['protocol_version' => 3], $this->url->getParams()) as $option => $value) {
$set= self::$options[$option];
if (!$set($this->handle, $value)) {
ldap_unbind($this->handle);
$this->handle= null;
throw new LDAPException('Cannot set option "'.$option.'"', ldap_errno($this->handle));
}
}

if (null === $dn) {
$result= ldap_bind($this->handle, $this->url->getUser(null), $this->url->getPassword(null));
} else {
$result= ldap_bind($this->handle, $dn, $password->reveal());
}
if (false === $result) {
$error= ldap_errno($this->handle);
ldap_unbind($this->handle);
$this->handle= null;
if (LDAP_SERVER_DOWN === $error || -1 === $error) {
throw new ConnectException('Cannot connect to '.$uri);
} else {
throw new LDAPException('Cannot bind for "'.($dn ?: $this->url->getUser(null)).'"', $error);
}
}

return $this;
}

Expand All @@ -127,7 +84,7 @@ public function connect($dn= null, Secret $password= null) {
* @return bool
*/
public function isConnected() {
return is_resource($this->handle);
return $this->proto->connected();
}

/**
Expand All @@ -136,10 +93,7 @@ public function isConnected() {
* @see php://ldap_close
*/
public function close() {
if ($this->handle) {
ldap_unbind($this->handle);
$this->handle= null;
}
$this->proto->close();
}

/**
Expand All @@ -149,11 +103,11 @@ public function close() {
* @return peer.ldap.LDAPException
*/
private function error($message) {
$error= ldap_errno($this->handle);
$error= ldap_errno($this->proto->handle);
switch ($error) {
case -1: case LDAP_SERVER_DOWN:
ldap_unbind($this->handle);
$this->handle= null;
ldap_unbind($this->proto->handle);
$this->proto->handle= null;
return new LDAPDisconnected($message, $error);

case LDAP_NO_SUCH_OBJECT:
Expand All @@ -179,20 +133,17 @@ private function error($message) {
* @see php://ldap_search
*/
public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimit= 0, $timeLimit= 0, $deref= LDAP_DEREF_NEVER) {
if (false === ($res= ldap_search(
$this->handle,
return new LDAPSearchResult($this->proto->search(
LDAPQuery::SCOPE_SUB,
$base,
$filter,
$attributes,
$attrsOnly,
$sizeLimit,
$timeLimit,
null,
$deref
))) {
throw $this->error('Search failed');
}

return new LDAPSearchResult(new LDAPEntries($this->handle, $res));
));
}

/**
Expand All @@ -202,37 +153,17 @@ public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimi
* @return peer.ldap.LDAPSearchResult search result object
*/
public function searchBy(LDAPQuery $filter) {
static $methods= [
LDAPQuery::SCOPE_BASE => 'ldap_read',
LDAPQuery::SCOPE_ONELEVEL => 'ldap_list',
LDAPQuery::SCOPE_SUB => 'ldap_search'
];

if (!isset($methods[$filter->getScope()])) {
throw new IllegalArgumentException('Scope '.$filter->getScope().' not supported');
}

$f= $methods[$filter->getScope()];
if (false === ($res= $f(
$this->handle,
return new LDAPSearchResult($this->proto->search(
$filter->getScope(),
$filter->getBase(),
$filter->getFilter(),
$filter->getAttrs(),
$filter->getAttrsOnly(),
$filter->getSizeLimit(),
$filter->getTimelimit(),
$filter->getSort(),
$filter->getDeref()
))) {
throw $this->error('Search failed');
}

if ($sort= $filter->getSort()) {
$entries= new SortedLDAPEntries($this->handle, $res, $sort);
} else {
$entries= new LDAPEntries($this->handle, $res);
}

return new LDAPSearchResult($entries);
));
}

/**
Expand All @@ -244,13 +175,13 @@ public function searchBy(LDAPQuery $filter) {
* @throws peer.ldap.LDAPException
*/
public function read(LDAPEntry $entry) {
$res= ldap_read($this->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
if (LDAP_SUCCESS != ldap_errno($this->handle)) {
$res= ldap_read($this->proto->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
if (LDAP_SUCCESS != ldap_errno($this->proto->handle)) {
throw $this->error('Read "'.$entry->getDN().'" failed');
}

$entry= ldap_first_entry($this->handle, $res);
return LDAPEntry::create(ldap_get_dn($this->handle, $entry), ldap_get_attributes($this->handle, $entry));
$entry= ldap_first_entry($this->proto->handle, $res);
return LDAPEntry::create(ldap_get_dn($this->proto->handle, $entry), ldap_get_attributes($this->proto->handle, $entry));
}

/**
Expand All @@ -260,15 +191,15 @@ public function read(LDAPEntry $entry) {
* @return bool TRUE if the entry exists
*/
public function exists(LDAPEntry $entry) {
$res= ldap_read($this->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
$res= ldap_read($this->proto->handle, $entry->getDN(), 'objectClass=*', [], false, 0);

// Check for certain error code (#32)
if (LDAP_NO_SUCH_OBJECT === ldap_errno($this->handle)) {
if (LDAP_NO_SUCH_OBJECT === ldap_errno($this->proto->handle)) {
return false;
}

// Check for other errors
if (LDAP_SUCCESS != ldap_errno($this->handle)) {
if (LDAP_SUCCESS != ldap_errno($this->proto->handle)) {
throw $this->error('Read "'.$entry->getDN().'" failed');
}

Expand All @@ -289,7 +220,7 @@ public function add(LDAPEntry $entry) {

// This actually returns NULL on failure, not FALSE, as documented
if (null == ($res= ldap_add(
$this->handle,
$this->proto->handle,
$entry->getDN(),
$entry->getAttributes()
))) {
Expand All @@ -312,7 +243,7 @@ public function add(LDAPEntry $entry) {
*/
public function modify(LDAPEntry $entry) {
if (false == ($res= ldap_modify(
$this->handle,
$this->proto->handle,
$entry->getDN(),
$entry->getAttributes()
))) {
Expand All @@ -332,7 +263,7 @@ public function modify(LDAPEntry $entry) {
*/
public function delete(LDAPEntry $entry) {
if (false == ($res= ldap_delete(
$this->handle,
$this->proto->handle,
$entry->getDN()
))) {
throw $this->error('Delete for "'.$entry->getDN().'" failed');
Expand All @@ -351,7 +282,7 @@ public function delete(LDAPEntry $entry) {
*/
public function addAttribute(LDAPEntry $entry, $name, $value) {
if (false == ($res= ldap_mod_add(
$this->handle,
$this->proto->handle,
$entry->getDN(),
[$name => $value]
))) {
Expand All @@ -370,7 +301,7 @@ public function addAttribute(LDAPEntry $entry, $name, $value) {
*/
public function deleteAttribute(LDAPEntry $entry, $name) {
if (false == ($res= ldap_mod_del(
$this->handle,
$this->proto->handle,
$entry->getDN(),
$name
))) {
Expand All @@ -390,7 +321,7 @@ public function deleteAttribute(LDAPEntry $entry, $name) {
*/
public function replaceAttribute(LDAPEntry $entry, $name, $value) {
if (false == ($res= ldap_mod_replace(
$this->handle,
$this->proto->handle,
$entry->getDN(),
[$name => $value]
))) {
Expand All @@ -399,4 +330,20 @@ public function replaceAttribute(LDAPEntry $entry, $name, $value) {

return $res;
}

/** @return string */
public function toString() { return nameof($this).'('.$this->proto->connection().')'; }

/** @return string */
public function hashCode() { return 'C'.$this->proto->id(); }

/**
* Compare
*
* @param var $value
* @return int
*/
public function compareTo($value) {
return $value === $this ? 0 : 1;
}
}
11 changes: 11 additions & 0 deletions src/main/php/peer/ldap/filter/AndFilter.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php namespace peer\ldap\filter;

class AndFilter implements Filter {
public $kind= 'and';

public $filters;

public function __construct(... $filters) {
$this->filters= $filters;
}
}
12 changes: 12 additions & 0 deletions src/main/php/peer/ldap/filter/ApproximateFilter.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php namespace peer\ldap\filter;

class ApproximateFilter implements Filter {
public $kind= 'approximate';

public $attribute, $value;

public function __construct($attribute, $value) {
$this->attribute= $attribute;
$this->value= $value;
}
}
12 changes: 12 additions & 0 deletions src/main/php/peer/ldap/filter/EqualityFilter.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php namespace peer\ldap\filter;

class EqualityFilter implements Filter {
public $kind= 'equality';

public $attribute, $value;

public function __construct($attribute, $value) {
$this->attribute= $attribute;
$this->value= $value;
}
}
13 changes: 13 additions & 0 deletions src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php namespace peer\ldap\filter;

class ExtensibleFilter implements Filter {
public $kind= 'extensible';

public $type, $rule, $value, $attributes;

public function __construct($type, $rule, $value, $attributes= false) {
$this->type= $type;
$this->rule= $rule;
$this->value= $value;
}
}
Loading