Skip to content

Commit 93f521b

Browse files
committed
Finish clean-up of initial old code implementation to current structure. This still does not represent any finished product. However, it does represent what was being worked on before this effort was previously dropped.
There's a lot to be redesigned here and tested. But this provides a cleaner starting point.
1 parent 1ed04c7 commit 93f521b

File tree

7 files changed

+383
-255
lines changed

7 files changed

+383
-255
lines changed

src/FreeDSx/Ldap/LdapClient.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use FreeDSx\Ldap\Operation\Request\ExtendedRequest;
2626
use FreeDSx\Ldap\Operation\Request\RequestInterface;
2727
use FreeDSx\Ldap\Operation\Request\SearchRequest;
28+
use FreeDSx\Ldap\Operation\Request\SyncRequest;
2829
use FreeDSx\Ldap\Operation\Response\CompareResponse;
2930
use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
3031
use FreeDSx\Ldap\Operation\Response\SearchResponse;
@@ -326,9 +327,12 @@ public function dirSync(
326327
/**
327328
* A helper for performing a ReplSync / directory synchronization as described in RFC4533.
328329
*/
329-
public function syncRepl(): SyncRepl
330+
public function syncRepl(SyncRequest $syncRequest): SyncRepl
330331
{
331-
return new SyncRepl($this);
332+
return new SyncRepl(
333+
$this,
334+
$syncRequest,
335+
);
332336
}
333337

334338
/**
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FreeDSx\Ldap\Operation\Request;
6+
7+
use FreeDSx\Ldap\Entry\Attribute;
8+
use FreeDSx\Ldap\Search\Filter\FilterInterface;
9+
use FreeDSx\Ldap\Search\SyncHandlerInterface;
10+
11+
class SyncRequest extends SearchRequest
12+
{
13+
private SyncHandlerInterface $syncHandler;
14+
15+
public function __construct(
16+
SyncHandlerInterface $syncHandler,
17+
FilterInterface $filter,
18+
string|Attribute ...$attributes
19+
) {
20+
$this->syncHandler = $syncHandler;
21+
22+
parent::__construct(
23+
$filter,
24+
...$attributes
25+
);
26+
}
27+
28+
public function getSyncHandler(): SyncHandlerInterface
29+
{
30+
return $this->syncHandler;
31+
}
32+
}

src/FreeDSx/Ldap/Operations.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
use FreeDSx\Ldap\Operation\Request\SaslBindRequest;
3434
use FreeDSx\Ldap\Operation\Request\SearchRequest;
3535
use FreeDSx\Ldap\Operation\Request\SimpleBindRequest;
36+
use FreeDSx\Ldap\Operation\Request\SyncRequest;
3637
use FreeDSx\Ldap\Operation\Request\UnbindRequest;
3738
use FreeDSx\Ldap\Protocol\ProtocolElementInterface;
3839
use FreeDSx\Ldap\Search\Filter\FilterInterface;
3940
use FreeDSx\Ldap\Search\Filters;
41+
use FreeDSx\Ldap\Search\SyncHandlerInterface;
4042
use Stringable;
4143

4244
/**
@@ -229,6 +231,21 @@ public static function search(
229231
);
230232
}
231233

234+
/**
235+
* Sync with LDAP (via method RFC-4533 / SyncRepl) with a given filter, scope, etc to retrieve a set of entries.
236+
*/
237+
public static function sync(
238+
SyncHandlerInterface $syncHandler,
239+
?FilterInterface $filter = null,
240+
Attribute|string ...$attributes,
241+
): SearchRequest {
242+
return new SyncRequest(
243+
$syncHandler,
244+
$filter ?? Filters::present('objectClass'),
245+
...$attributes
246+
);
247+
}
248+
232249
/**
233250
* Search for a specific base DN object to read. This sets a 'present' filter for the 'objectClass' attribute to help
234251
* simplify it.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the FreeDSx LDAP package.
7+
*
8+
* (c) Chad Sikorra <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace FreeDSx\Ldap\Protocol\ClientProtocolHandler;
15+
16+
use FreeDSx\Ldap\ClientOptions;
17+
use FreeDSx\Ldap\Control\Control;
18+
use FreeDSx\Ldap\Control\Sync\SyncDoneControl;
19+
use FreeDSx\Ldap\Control\Sync\SyncRequestControl;
20+
use FreeDSx\Ldap\Control\Sync\SyncStateControl;
21+
use FreeDSx\Ldap\Exception\ProtocolException;
22+
use FreeDSx\Ldap\Exception\RuntimeException;
23+
use FreeDSx\Ldap\Operation\LdapResult;
24+
use FreeDSx\Ldap\Operation\Request\SearchRequest;
25+
use FreeDSx\Ldap\Operation\Request\SyncRequest;
26+
use FreeDSx\Ldap\Operation\Response\SearchResultDone;
27+
use FreeDSx\Ldap\Operation\Response\SearchResultEntry;
28+
use FreeDSx\Ldap\Operation\Response\SearchResultReference;
29+
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncIdSet;
30+
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncNewCookie;
31+
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncRefreshDelete;
32+
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncRefreshPresent;
33+
use FreeDSx\Ldap\Operation\Response\SyncResponse;
34+
use FreeDSx\Ldap\Operation\Response\SyncResult;
35+
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
36+
use FreeDSx\Ldap\Protocol\LdapMessageResponse;
37+
use FreeDSx\Ldap\Protocol\Queue\ClientQueue;
38+
use FreeDSx\Ldap\Search\SyncHandlerInterface;
39+
40+
class ClientSyncHandler extends ClientBasicHandler
41+
{
42+
private ?SyncHandlerInterface $syncHandler;
43+
44+
private LdapMessageResponse $lastResponse;
45+
46+
/**
47+
* {@inheritDoc}
48+
*/
49+
public function handleRequest(ClientProtocolContext $context): ?LdapMessageResponse
50+
{
51+
/** @var SearchRequest $request */
52+
$request = $context->getRequest();
53+
if ($request->getBaseDn() === null) {
54+
$request->setBaseDn($context->getOptions()->getBaseDn() ?? null);
55+
}
56+
57+
return parent::handleRequest($context);
58+
}
59+
60+
/**
61+
* {@inheritDoc}
62+
*/
63+
public function handleResponse(
64+
LdapMessageRequest $messageTo,
65+
LdapMessageResponse $messageFrom,
66+
ClientQueue $queue,
67+
ClientOptions $options,
68+
): ?LdapMessageResponse {
69+
/** @var SyncRequest $searchRequest */
70+
$searchRequest = $messageTo->getRequest();
71+
$this->syncHandler = $searchRequest->getSyncHandler();
72+
73+
return $this->handle(
74+
$messageTo,
75+
$queue,
76+
);
77+
}
78+
79+
public function handle(
80+
LdapMessageRequest $messageTo,
81+
ClientQueue $queue,
82+
): ?LdapMessageResponse {
83+
$initial = [];
84+
$updates = [];
85+
$present = [];
86+
$deleted = [];
87+
$cookie = '';
88+
89+
$control = $messageTo->controls()->get(Control::OID_SYNC_REQUEST);
90+
if (!$control instanceof SyncRequestControl) {
91+
throw new RuntimeException('Expected a SyncRequestControl, but there is none.');
92+
}
93+
94+
$syncDone = null;
95+
if ($this->isInitialPollRequest($control)) {
96+
$initial = $this->initialContent(
97+
$messageTo,
98+
$queue
99+
);
100+
$syncDone = $this->getSyncDoneControl($this->lastResponse);
101+
if ($syncDone === null) {
102+
throw new ProtocolException('Expected a SyncDoneControl, but none was received.');
103+
}
104+
$cookie = $syncDone->getCookie();
105+
} elseif ($this->isContentUpdatePoll($control)) {
106+
$updates = $this->persistStage(
107+
$queue,
108+
$messageTo,
109+
$control,
110+
);
111+
$syncDone = $this->getSyncDoneControl($this->lastResponse);
112+
113+
if ($syncDone === null) {
114+
throw new ProtocolException('Expected a SyncDoneControl, but none was received.');
115+
}
116+
117+
$cookie = $syncDone->getCookie();
118+
if (!$cookie) {
119+
$cookie = $control->getCookie();
120+
}
121+
}
122+
123+
if ($this->isContentUpdatePoll($control) && $syncDone?->getRefreshDeletes()) {
124+
$deleted = $this->phaseDeleted(
125+
$queue,
126+
$messageTo
127+
);
128+
}
129+
130+
return new LdapMessageResponse(
131+
$this->lastResponse->getMessageId(),
132+
new SyncResponse(
133+
$this->getLdapResult($this->lastResponse),
134+
(string) $cookie,
135+
$present,
136+
$deleted,
137+
$initial,
138+
$updates,
139+
),
140+
...$this->lastResponse->controls()->toArray()
141+
);
142+
}
143+
144+
private function initialContent(
145+
LdapMessageRequest $messageTo,
146+
ClientQueue $queue
147+
): array {
148+
$results = [];
149+
$isDone = false;
150+
151+
foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
152+
$response = $message->getResponse();
153+
$syncResult = null;
154+
155+
if ($response instanceof SearchResultEntry) {
156+
$syncResult = new SyncResult(
157+
$response->getEntry(),
158+
$this->getSyncStateControl($message)
159+
);
160+
} elseif ($response instanceof SearchResultReference) {
161+
$syncResult = new SyncResult(
162+
$response->getReferrals(),
163+
$this->getSyncStateControl($message)
164+
);
165+
} elseif ($response instanceof SearchResultDone) {
166+
$isDone = true;
167+
} else {
168+
throw new ProtocolException('Unexpected message encountered during initial content sync.');
169+
}
170+
171+
if ($syncResult && $this->syncHandler) {
172+
$this->syncHandler->initialPoll($syncResult);
173+
} elseif ($syncResult) {
174+
$results[] = $syncResult;
175+
}
176+
if ($isDone) {
177+
$this->lastResponse = $message;
178+
break;
179+
}
180+
}
181+
182+
return $results;
183+
}
184+
185+
private function phaseDeleted(
186+
ClientQueue $queue,
187+
LdapMessageRequest $messageTo
188+
): array {
189+
$results = [];
190+
191+
foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
192+
$response = $message->getResponse();
193+
194+
if ($response instanceof SearchResultEntry) {
195+
$results[] = new SyncResult(
196+
$response->getEntry(),
197+
$this->getSyncStateControl($message),
198+
);
199+
} elseif ($response instanceof SearchResultReference) {
200+
$results[] = new SyncResult(
201+
$response->getReferrals(),
202+
$this->getSyncStateControl($message),
203+
);
204+
} elseif ($response instanceof SearchResultDone) {
205+
$this->lastResponse = $message;
206+
break;
207+
}
208+
}
209+
210+
return $results;
211+
}
212+
213+
private function persistStage(
214+
ClientQueue $queue,
215+
LdapMessageRequest $messageTo,
216+
SyncRequestControl $syncRequest,
217+
): array {
218+
$isDone = false;
219+
$syncInfoDelete = null;
220+
$syncInfoPresent = null;
221+
$syncNewCookie = null;
222+
$message = null;
223+
224+
$syncResults = [];
225+
do {
226+
foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
227+
$this->lastResponse = $message;
228+
$response = $message->getResponse();
229+
230+
if ($response instanceof SyncRefreshDelete) {
231+
$syncInfoDelete = $response;
232+
$syncRequest->setCookie($syncInfoDelete->getCookie());
233+
$queue->sendMessage($messageTo);
234+
} elseif ($response instanceof SyncRefreshPresent) {
235+
$syncInfoPresent = $response;
236+
$syncRequest->setCookie($syncInfoPresent->getCookie());
237+
$queue->sendMessage($messageTo);
238+
} elseif ($response instanceof SyncNewCookie) {
239+
$syncNewCookie = $response;
240+
$syncRequest->setCookie($syncNewCookie->getCookie());
241+
} elseif ($response instanceof SyncIdSet) {
242+
} else {
243+
if ($response instanceof SearchResultEntry) {
244+
$syncResults[] = new SyncResult(
245+
$response->getEntry(),
246+
$this->getSyncStateControl($message)
247+
);
248+
} elseif ($response instanceof SearchResultReference) {
249+
$syncResults[] = new SyncResult(
250+
$response->getReferrals(),
251+
$this->getSyncStateControl($message)
252+
);
253+
} elseif ($response instanceof SearchResultDone) {
254+
$isDone = true;
255+
break;
256+
}
257+
}
258+
}
259+
} while (!$isDone);
260+
261+
return $syncResults;
262+
}
263+
264+
private function isInitialPollRequest(SyncRequestControl $control): bool
265+
{
266+
return empty($control->getCookie())
267+
&& $control->getMode() === SyncRequestControl::MODE_REFRESH_ONLY;
268+
}
269+
270+
private function isContentUpdatePoll(SyncRequestControl $control): bool
271+
{
272+
return !empty($control->getCookie());
273+
}
274+
275+
private function getSyncStateControl(LdapMessageResponse $response): ?SyncStateControl
276+
{
277+
$syncState = $response->controls()->get(Control::OID_SYNC_STATE);
278+
279+
return $syncState instanceof SyncStateControl
280+
? $syncState
281+
: null;
282+
}
283+
284+
private function getSyncDoneControl(LdapMessageResponse $response): ?SyncDoneControl
285+
{
286+
$syncDone = $response->controls()->get(Control::OID_SYNC_DONE);
287+
288+
return $syncDone instanceof SyncDoneControl
289+
? $syncDone
290+
: null;
291+
}
292+
293+
private function getLdapResult(LdapMessageResponse $response): LdapResult
294+
{
295+
$result = $response->getResponse();
296+
297+
if (!$result instanceof LdapResult) {
298+
throw new RuntimeException(sprintf(
299+
'Expected an LdapResult, but received "%s".',
300+
$result::class
301+
));
302+
}
303+
304+
return $result;
305+
}
306+
}

0 commit comments

Comments
 (0)