Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions src/FreeDSx/Ldap/LdapServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use FreeDSx\Ldap\Exception\RuntimeException;
use FreeDSx\Ldap\Ldif\LdifParser;
use FreeDSx\Ldap\Ldif\Loader\LdifLoaderInterface;
use FreeDSx\Ldap\Ldif\Output\LdifOutputInterface;
use FreeDSx\Ldap\Schema\SchemaValidationMode;
use FreeDSx\Ldap\Schema\Validation\SchemaValidator;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
Expand All @@ -30,6 +31,8 @@
use FreeDSx\Ldap\Server\Backend\Auth\PasswordAuthenticatableInterface;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\Storage\EntryStorageInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Export\DirectoryDumper;
use FreeDSx\Ldap\Server\Backend\Storage\Export\DumpOptions;
use FreeDSx\Ldap\Server\Backend\Storage\LdapImporter;
use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend;
use FreeDSx\Ldap\Server\Backend\Write\WriteHandlerInterface;
Expand Down Expand Up @@ -220,6 +223,33 @@ public function applyChanges(LdifLoaderInterface $loader): self
return $this;
}

/**
* Streams the configured storage backend's entries as RFC 2849 LDIF content records to the given output.
*
* Symmetric with {@see seed()}: the produced LDIF re-seeds the directory verbatim, including operational
* attributes.
*
* @throws RuntimeException when no storage backend is configured
*/
public function dump(
LdifOutputInterface $output,
DumpOptions $options = new DumpOptions(),
): self {
$backend = $this->options->getBackend();

if (!$backend instanceof WritableStorageBackend) {
throw new RuntimeException('dump() requires a storage backend configured via useStorage().');
}

$output->write((new DirectoryDumper(
$backend,
$this->options->getDseNamingContexts(),
$this->options->getFilterEvaluator(),
))->dump($options));

return $this;
}

private function buildSchemaValidator(): ?SchemaValidator
{
$mode = $this->options->getSchemaValidationMode();
Expand Down
18 changes: 13 additions & 5 deletions src/FreeDSx/Ldap/Ldif/LdifWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,28 @@ public function write(iterable $requests): string
$blocks = [];

foreach ($requests as $request) {
$blocks[] = $this->requestBlock($request);
$blocks[] = $this->writeOne($request);
}

$body = implode($this->options->getLineEnding(), $blocks);
return $this->versionHeader() . implode($this->options->getLineEnding(), $blocks);
}

/**
* Returns the LDIF "version: 1" header (with trailing blank line) when enabled, otherwise empty.
*/
public function versionHeader(): string
{
return $this->options->isIncludeVersion()
? 'version: 1' . $this->options->getLineEnding() . $this->options->getLineEnding() . $body
: $body;
? 'version: 1' . $this->options->getLineEnding() . $this->options->getLineEnding()
: '';
}

/**
* Serializes a single request to its LDIF block (ending with the configured line ending).
*
* @throws InvalidArgumentException
*/
private function requestBlock(RequestInterface $request): string
public function writeOne(RequestInterface $request): string
{
return match (true) {
$request instanceof AddRequest => $this->addBlock($request),
Expand Down
70 changes: 70 additions & 0 deletions src/FreeDSx/Ldap/Ldif/Output/FileLdifOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Ldif\Output;

use FreeDSx\Ldap\Exception\RuntimeException;

use function dirname;
use function fclose;
use function fopen;
use function fwrite;
use function is_dir;
use function is_writable;
use function sprintf;

/**
* Streams LDIF chunks to a file path.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class FileLdifOutput implements LdifOutputInterface
{
public function __construct(private string $path) {}

/**
* @param iterable<string> $chunks
* @throws RuntimeException when the file cannot be opened or written
*/
public function write(iterable $chunks): void
{
$dir = dirname($this->path);

if (!is_dir($dir) || !is_writable($dir)) {
throw new RuntimeException(sprintf(
'Unable to open "%s" for writing.',
$this->path,
));
}

$handle = fopen($this->path, 'w');

if ($handle === false) {
throw new RuntimeException(sprintf(
'Unable to open "%s" for writing.',
$this->path,
));
}

try {
foreach ($chunks as $chunk) {
fwrite(
$handle,
$chunk,
);
}
} finally {
fclose($handle);
}
}
}
27 changes: 27 additions & 0 deletions src/FreeDSx/Ldap/Ldif/Output/LdifOutputInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Ldif\Output;

/**
* Destination for streamed LDIF chunks produced by {@see \FreeDSx\Ldap\LdapServer::dump()}.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
interface LdifOutputInterface
{
/**
* @param iterable<string> $chunks
*/
public function write(iterable $chunks): void;
}
46 changes: 46 additions & 0 deletions src/FreeDSx/Ldap/Ldif/Output/StringLdifOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Ldif\Output;

use Stringable;

/**
* Accumulates LDIF chunks into an in-memory string.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final class StringLdifOutput implements LdifOutputInterface, Stringable
{
private string $ldif = '';

/**
* @param iterable<string> $chunks
*/
public function write(iterable $chunks): void
{
foreach ($chunks as $chunk) {
$this->ldif .= $chunk;
}
}

public function getLdif(): string
{
return $this->ldif;
}

public function __toString(): string
{
return $this->ldif;
}
}
4 changes: 2 additions & 2 deletions src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private function getSearchHandler(): ServerProtocolHandler\ServerSearchHandler
return new ServerProtocolHandler\ServerSearchHandler(
queue: $this->queue,
backend: $this->handlerFactory->makeBackend(),
filterEvaluator: $this->handlerFactory->makeFilterEvaluator(),
filterEvaluator: $this->options->getFilterEvaluator(),
accessControl: $this->options->getAccessControl(),
schema: $this->options->getSchema(),
limits: $this->options->makeSearchLimits(),
Expand Down Expand Up @@ -151,7 +151,7 @@ private function getPagingHandler(): ServerProtocolHandler\ServerPagingHandler
return new ServerProtocolHandler\ServerPagingHandler(
queue: $this->queue,
backend: $this->handlerFactory->makeBackend(),
filterEvaluator: $this->handlerFactory->makeFilterEvaluator(),
filterEvaluator: $this->options->getFilterEvaluator(),
accessControl: $this->options->getAccessControl(),
requestHistory: $this->requestHistory,
schema: $this->options->getSchema(),
Expand Down
106 changes: 106 additions & 0 deletions src/FreeDSx/Ldap/Server/Backend/Storage/Export/DirectoryDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Storage\Export;

use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Ldif\LdifWriter;
use FreeDSx\Ldap\Operations;
use FreeDSx\Ldap\Search\Filter\AndFilter;
use FreeDSx\Ldap\Search\Filter\FilterInterface;
use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluator;
use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\StorageListOptions;
use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend;
use Generator;

/**
* Streams the entries of a writable storage backend as LDIF content-record chunks for backup/export.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class DirectoryDumper
{
/**
* @param string[] $namingContexts dump roots when DumpOptions::baseDn is not set
*/
public function __construct(
private WritableStorageBackend $backend,
private array $namingContexts,
private FilterEvaluatorInterface $filterEvaluator = new FilterEvaluator(),
private LdifWriter $writer = new LdifWriter(),
) {}

/**
* @return iterable<string>
*/
public function dump(DumpOptions $options = new DumpOptions()): iterable
{
$header = $this->writer->versionHeader();

if ($header !== '') {
yield $header;
}

$filter = $options->getFilter();

foreach ($this->resolveBases($options) as $base) {
yield from $this->streamNamingContext(
$base,
$filter,
);
}
}

/**
* @return list<Dn>
*/
private function resolveBases(DumpOptions $options): array
{
if ($options->getBaseDn() !== null) {
return [$options->getBaseDn()];
}

return array_values(array_map(
fn(string $namingContext): Dn => new Dn($namingContext),
$this->namingContexts,
));
}

/**
* Stream the entries from a naming context to LDIF.
*
* @return Generator<string>
*/
private function streamNamingContext(
Dn $base,
?FilterInterface $filter,
): Generator {
$listOptions = new StorageListOptions(
baseDn: $base,
subtree: true,
filter: $filter ?? new AndFilter(),
);
$stream = $this->backend->getStorage()->list($listOptions);

foreach ($stream->entries as $entry) {
if (!$stream->isPreFiltered && $filter !== null) {
if (!$this->filterEvaluator->evaluate($entry, $filter)) {
continue;
}
}

yield $this->writer->writeOne(Operations::add($entry));
}
}
}
Loading
Loading