diff --git a/.env.testing b/.env.testing
index ef55ac0f..313f8c9f 100644
--- a/.env.testing
+++ b/.env.testing
@@ -3,7 +3,7 @@ APP_ENV=local
APP_KEY=
APP_DEBUG=true
-LOG_CHANNEL=stderr
+LOG_CHANNEL=single
CACHE_DRIVER=array
QUEUE_CONNECTION=sync
diff --git a/app/Classes/LDAP/Attribute.php b/app/Classes/LDAP/Attribute.php
index 3b144083..f0403bb4 100644
--- a/app/Classes/LDAP/Attribute.php
+++ b/app/Classes/LDAP/Attribute.php
@@ -190,6 +190,21 @@ class Attribute implements \Countable, \ArrayAccess
->get($tag,[]),$values)));
}
+ /**
+ * If this attribute has changes, re-render the attribute values
+ *
+ * @return array
+ */
+ public function getDirty(): array
+ {
+ $dirty = [];
+
+ if ($this->isDirty())
+ $dirty = [$this->name_lc => $this->_values->toArray()];
+
+ return $dirty;
+ }
+
/**
* Return the hints about this attribute, ie: RDN, Required, etc
*
diff --git a/app/Classes/LDAP/Import.php b/app/Classes/LDAP/Import.php
index 61525508..6dfa783a 100644
--- a/app/Classes/LDAP/Import.php
+++ b/app/Classes/LDAP/Import.php
@@ -56,6 +56,7 @@ abstract class Import
{
switch ($action) {
case static::LDAP_IMPORT_ADD:
+ case static::LDAP_IMPORT_MODIFY:
try {
$o->save();
@@ -65,15 +66,17 @@ abstract class Import
if ($e->getDetailedError())
return collect([
'dn'=>$o->getDN(),
- 'result'=>sprintf('%d: %s (%s)',
+ 'link'=>$o->getDNSecure(),
+ 'result'=>sprintf('%d: %s%s',
($x=$e->getDetailedError())->getErrorCode(),
$x->getErrorMessage(),
- $x->getDiagnosticMessage(),
+ $x->getDiagnosticMessage() ? ' ('.$x->getDiagnosticMessage().')' : '',
)
]);
else
return collect([
'dn'=>$o->getDN(),
+ 'link'=>$o->getDNSecure(),
'result'=>sprintf('%d: %s',
$e->getCode(),
$e->getMessage(),
@@ -81,9 +84,13 @@ abstract class Import
]);
}
- Log::debug(sprintf('%s:Import Commited',self::LOGKEY));
+ Log::debug(sprintf('%s:= Import Commited',self::LOGKEY));
- return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);
+ return collect([
+ 'dn'=>$o->getDN(),
+ 'link'=>$o->getDNSecure(),
+ 'result'=>$action === self::LDAP_IMPORT_ADD ? __('Created') : __('Modified'),
+ ]);
default:
throw new GeneralException('Unhandled action during commit: '.$action);
diff --git a/app/Classes/LDAP/Import/LDIF.php b/app/Classes/LDAP/Import/LDIF.php
index fc4e5896..d5e7b7ae 100644
--- a/app/Classes/LDAP/Import/LDIF.php
+++ b/app/Classes/LDAP/Import/LDIF.php
@@ -24,7 +24,9 @@ class LDIF extends Import
public function process(): Collection
{
$c = 0;
- $action = NULL;
+ $action = self::LDAP_IMPORT_ADD; // Assume add mode
+ $subaction = 'add'; // Assume add
+ $dn = NULL;
$attribute = NULL;
$base64encoded = FALSE;
$o = NULL;
@@ -32,23 +34,38 @@ class LDIF extends Import
$version = NULL;
$result = collect();
- // @todo When renaming DNs, the hotlink should point to the new entry on success, or the old entry on failure.
foreach (preg_split('/(\r?\n|\r)/',$this->input) as $line) {
$c++;
- Log::debug(sprintf('%s:LDIF Line [%s]',self::LOGKEY,$line));
+ Log::debug(sprintf('%s:LDIF Line [%s] (%d)',self::LOGKEY,$line,$c));
$line = trim($line);
// If the line starts with a comment, ignore it
if (preg_match('/^#/',$line))
continue;
+ // If the line starts with a dash, its more updates to the same entry
+ if (preg_match('/^-/',$line)) {
+ Log::debug(sprintf('%s:/ DASH Line [%s] (%d)',self::LOGKEY,$line,$c),['action'=>$action,'subaction'=>$subaction,'attribute'=>$attribute,'value'=>$value]);
+ if ($attribute)
+ $o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
+
+ $base64encoded = FALSE;
+ $attribute = NULL;
+ $value = '';
+ continue;
+ }
+
// If we have a blank line, then that completes this command
if (! $line) {
- if (! is_null($o)) {
- // Add the last attribute;
- $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
+ // If we havent got a version yet, then we havent started
+ if (! $version) {
+ continue;
- Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
+ } elseif (! is_null($o)) {
+ if ($attribute)
+ $o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
+
+ Log::debug(sprintf('%s:- Committing Entry (More) [%s]',self::LOGKEY,$o->getDN()));
// Commit
$result->push($this->commit($o,$action));
@@ -59,27 +76,123 @@ class LDIF extends Import
$base64encoded = FALSE;
$attribute = NULL;
$value = '';
+
+ } else {
+ throw new GeneralException(sprintf('Processing Error - Line exists [%s] on (%d) but object is NULL',$line,$c));
}
continue;
}
$m = [];
- preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
+ preg_match('/^([a-zA-Z0-9;-]+)(:+)\s*(.*)$/',$line,$m);
+
+ // If $m is NULL, then this is the 2nd (or more) line of a base64 encoded value
+ if (! $m) {
+ $value .= $line;
+ Log::debug(sprintf('%s:+ attribute [%s] appending [%s] (%d)',self::LOGKEY,$attribute,$line,$c));
+
+ // add to last attr value
+ continue;
+
+ } else {
+ // If base64 mode was enabled, and there is a following attribute after a base64encoded attribute, it hasnt been processed yet
+ if ($base64encoded) {
+ Log::debug(sprintf('%s:- Completing base64 attribute [%s]',self::LOGKEY,$attribute));
+
+ $o = $this->entry($o,$attribute,$subaction,base64_decode($value),$c);
+
+ $attribute = NULL;
+ }
+
+ $base64encoded = ($m[2] === '::');
+ $value = $m[3];
+
+ // If we are base64encoded, we need to loop around
+ if ($base64encoded) {
+ $attribute = $m[1];
+ Log::debug(sprintf('%s:/ Retrieving base64 attribute [%s] (%c)',self::LOGKEY,$attribute,$c));
+
+ continue;
+ }
+ }
+
+ // changetype needs to be after the dn, and if it isnt we'll assume add.
+ if ($dn && Arr::get($m,1) !== 'changetype') {
+ if ($action === self::LDAP_IMPORT_ADD) {
+ Log::debug(sprintf('%s:Creating new entry [%s]:',self::LOGKEY,$dn),['o'=>$o]);
+ $o = new Entry;
+ $o->setDn($dn);
+ $dn = NULL;
+
+ } else {
+ Log::debug(sprintf('%s:Looking for existing entry [%s]:',self::LOGKEY,$dn),['o'=>$o]);
+ $o = Entry::find($dn);
+ $dn = NULL;
+
+ if (! $o) {
+ $result->push(collect(['dn'=>$dn,'result'=>__('Entry doesnt exist')]));
+ $result->last()->put('line',$c);;
+
+ continue;
+ }
+ }
+ }
switch (Arr::get($m,1)) {
+ case 'dn':
+ $dn = $base64encoded ? base64_decode($value) : $value;
+ Log::debug(sprintf('%s:# Got DN [%s]:',self::LOGKEY,$dn));
+
+ $value = '';
+ $base64encoded = FALSE;
+ break;
+
case 'changetype':
if ($m[2] !== ':')
- throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
+ throw new GeneralException(sprintf('changetype cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
+
+ if (! is_null($o))
+ throw new GeneralException(sprintf('Previous Entry not complete? (line %d)',$c));
+
+ Log::debug(sprintf('%s:- Action [%s]',self::LOGKEY,$m[3]));
switch ($m[3]) {
- // if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) {
+ case 'add':
+ $action = self::LDAP_IMPORT_ADD;
+ break;
+
+ case 'modify':
+ $action = self::LDAP_IMPORT_MODIFY;
+ break;
+
+ /*
+ case 'delete':
+ $action = self::LDAP_IMPORT_DELETE;
+ break;
+ */
+
+ // @todo modrdn|moddn
default:
throw new NotImplementedException(sprintf('Unknown change type [%s]? (line %d)',$m[3],$c));
}
break;
+ case 'add':
+ case 'replace':
+ if ($action !== self::LDAP_IMPORT_MODIFY)
+ throw new GeneralException(sprintf('%s action can only be used with changetype: modify (line %d)',$m[1],$c));
+
+ $subaction = $m[1];
+ break;
+
+ case 'delete':
+ $subaction = $m[1];
+ $attribute = $m[3];
+ $value = NULL;
+ break;
+
case 'version':
if (! is_null($version))
throw new VersionException(sprintf('Version has already been set at [%d]. (line %d)',$version,$c));
@@ -92,49 +205,14 @@ class LDIF extends Import
// Treat it as an attribute
default:
- // If $m is NULL, then this is the 2nd (or more) line of a base64 encoded value
- if (! $m) {
- $value .= $line;
- Log::debug(sprintf('%s:- Attribute [%s] adding [%s] (%d)',self::LOGKEY,$attribute,$line,$c));
-
- // add to last attr value
- continue 2;
- }
+ // Start of a new attribute
+ $attribute = $m[1];
+ Log::debug(sprintf('%s:- Working with Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
// We are ready to create the entry or add the attribute
- if ($attribute) {
- if ($attribute === 'dn') {
- if (! is_null($o))
- throw new GeneralException(sprintf('Previous Entry not complete? (line %d)',$c));
-
- $dn = $base64encoded ? base64_decode($value) : $value;
- Log::debug(sprintf('%s:Creating new entry:',self::LOGKEY,$dn));
- //$o = Entry::find($dn);
-
- // If it doesnt exist, we'll create it
- //if (! $o) {
- $o = new Entry;
- $o->setDn($dn);
- //}
-
- $action = self::LDAP_IMPORT_ADD;
-
- } else {
- Log::debug(sprintf('%s:Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
-
- if ($value)
- $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
- else
- throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
- }
- }
-
- // Start of a new attribute
- $base64encoded = ($m[2] === '::');
- $attribute = $m[1];
- $value = $m[3];
-
- Log::debug(sprintf('%s:- New Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
+ $o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
+ $attribute = NULL;
+ $value = NULL;
}
if ($version !== 1)
@@ -142,11 +220,11 @@ class LDIF extends Import
}
// We may still have a pending action
- if ($action) {
- // Add the last attribute;
- $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
+ if ($o) {
+ if ($attribute)
+ $o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
- Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
+ Log::debug(sprintf('%s:- Committing Entry (Final) [%s]',self::LOGKEY,$o->getDN()));
// Commit
$result->push($this->commit($o,$action));
@@ -156,75 +234,46 @@ class LDIF extends Import
return $result;
}
- public function xreadEntry() {
- static $haveVersion = FALSE;
+ private function entry(Entry $o,string $attribute,string $subaction,?string $value,int $c): Entry
+ {
+ Log::debug(sprintf('%s:. %s Attribute [%s] value [%s] (%d)',self::LOGKEY,$subaction,$attribute,$value,$c));
- if ($lines = $this->nextLines()) {
+ switch ($subaction) {
+ case 'add':
+ if (! $value)
+ throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
- $server = $this->getServer();
+ $o->addAttributeItem($attribute,$value);
+ break;
- # The first line should be the DN
- if (preg_match('/^dn:/',$lines[0])) {
- list($text,$dn) = $this->getAttrValue(array_shift($lines));
+ case 'replace':
+ if (! $value)
+ throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
- # The second line should be our changetype
- if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) {
- $attrvalue = $this->getAttrValue($lines[0]);
- $changetype = $attrvalue[1];
- array_shift($lines);
+ if (! ($x=$o->getObject(($xx=strstr($attribute,';',TRUE)) ?: $attribute)))
+ throw new \Exception(sprintf('Attribute [%s] doesnt exist in [%s] (line %d)',$attribute,$o->getDn(),$c));
+
+ // If the attribute has changed, we'll assume this is an additional value for it
+ if ($x->isDirty()) {
+ Log::debug(sprintf('%s:/ Attribute [%s] has changed, assuming add',self::LOGKEY,$attribute));
+ $o->addAttributeItem($attribute,$value);
} else
- $changetype = 'add';
+ $o->{$attribute} = [Entry::TAG_NOTAG=>[$value]];
- $this->template = new Template($this->server_id,NULL,NULL,$changetype);
+ break;
- switch ($changetype) {
- case 'add':
- $rdn = get_rdn($dn);
- $container = $server->getContainer($dn);
+ case 'delete':
+ if (! $o->getObject($attribute))
+ throw new \Exception(sprintf('Attribute [%s] doesnt exist in [%s] (line %d)',$attribute,$o->getDn(),$c));
- $this->template->setContainer($container);
- $this->template->accept();
+ $o->{$attribute} = [];
+ break;
- $this->getAddDetails($lines);
- $this->template->setRDNAttributes($rdn);
+ default:
+ throw new \Exception('Unknown subaction:'.$subaction);
+ }
- return $this->template;
-
- break;
-
- case 'modify':
- if (! $server->dnExists($dn))
- return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines);
-
- $this->template->setDN($dn);
- $this->template->accept(FALSE,TRUE);
-
- return $this->getModifyDetails($lines);
-
- break;
-
- case 'moddn':
- case 'modrdn':
- if (! $server->dnExists($dn))
- return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines);
-
- $this->template->setDN($dn);
- $this->template->accept();
-
- return $this->getModRDNAttributes($lines);
-
- break;
-
- default:
- if (! $server->dnExists($dn))
- return $this->error(_('Unknown change type'),$lines);
- }
-
- } else
- return $this->error(_('A valid dn line is required'),$lines);
-
- } else
- return FALSE;
+ return $o;
}
}
\ No newline at end of file
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 36796ed8..19111910 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -538,9 +538,13 @@ class HomeController extends Controller
$result = $import->process();
} catch (NotImplementedException $e) {
+ Log::error(sprintf('Import Exception [%s]',$e->getMessage()));
+
abort(555,$e->getMessage());
} catch (\Exception $e) {
+ Log::error(sprintf('Import Exception [%s]',$e->getMessage()));
+
abort(598,$e->getMessage());
}
diff --git a/app/Ldap/Entry.php b/app/Ldap/Entry.php
index d266d549..084b4f1e 100644
--- a/app/Ldap/Entry.php
+++ b/app/Ldap/Entry.php
@@ -101,6 +101,30 @@ class Entry extends Model
->toArray();
}
+ /**
+ * This replaces the model's get dirty, given that we store LDAP attributes in $this->objects, replacing
+ * $this->original/$this->attributes
+ *
+ * @return array
+ */
+ public function getDirty(): array
+ {
+ $result = collect();
+
+ foreach ($this->objects as $o)
+ if ($o->isDirty())
+ $result = $result->merge($o->getDirty());
+
+ $result = $result
+ ->flatMap(function($item,$attr) {
+ return ($x=collect($item))->count()
+ ? $x->flatMap(fn($v,$k)=>[strtolower($attr.(($k !== self::TAG_NOTAG) ? ';'.$k : ''))=>$v])
+ : [strtolower($attr)=>[]];
+ });
+
+ return $result->toArray();
+ }
+
/**
* Determine if the new and old values for a given key are equivalent.
*/
@@ -454,7 +478,7 @@ class Entry extends Model
$ot = $this->getOtherTags();
$cache[$tag ?: '_all_'] = $this->objects
- ->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === Entry::TAG_NOTAG)))
+ ->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === self::TAG_NOTAG)))
->filter(fn($item)=>(! $tag) || $ot->has($item->name_lc) || count($item->tagValues($tag)) > 0);
}
diff --git a/resources/views/frames/import_result.blade.php b/resources/views/frames/import_result.blade.php
index acd8b04b..cc24baca 100644
--- a/resources/views/frames/import_result.blade.php
+++ b/resources/views/frames/import_result.blade.php
@@ -30,7 +30,13 @@
@foreach($result as $item)
- {{ $item->get('dn') }} |
+
+ @if($x=$item->get('link'))
+ {{ $item->get('dn') }}
+ @else
+ {{ $item->get('dn') }}
+ @endif
+ |
{{ $item->get('result') }} |
{{ $item->get('line') }} |
diff --git a/tests/Feature/ImportTest.php b/tests/Feature/ImportTest.php
index 1c53b7c6..4336b3fb 100644
--- a/tests/Feature/ImportTest.php
+++ b/tests/Feature/ImportTest.php
@@ -7,8 +7,11 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase;
+use App\Ldap\Entry;
+
class ImportTest extends TestCase
{
+ // Test delete and create an entry
public function testLDIF_Import()
{
$dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
@@ -27,7 +30,7 @@ class ImportTest extends TestCase
$x->delete();
$this->assertEquals(NULL,config('server')->fetch($dn));
- $file = new UploadedFile($import_file,'ldif-import.ldif',null,null,true);
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
$response = $this
->actingAs(Auth::user())
@@ -44,5 +47,256 @@ class ImportTest extends TestCase
// Check that it hsa been created
$this->assertEquals($dn,$x=config('server')->fetch($dn));
$this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+ $this->assertCount(4,$x->getObject('objectClass'));
+ $this->assertCount(0,array_diff(['inetOrgPerson','posixAccount','top','shadowAccount'],$x->getObject('objectClass')->values->toArray()));
+ $this->assertCount(1,$x->getObject('mail'));
+ $this->assertContains(Entry::TAG_NOTAG.'.0',$x->getObject('mail')->values->dot()->keys());
+ $this->assertContains('bart.simpson@example.com',$x->getObject('mail')->values->dot());
+ $this->assertEquals(3024,strlen($x->getObject('jpegphoto')->values->dot()->first()));
+ }
+
+ public function testLDIF_Import_Replace() {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.1.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertCount(1,$x->getObject('mail')->values);
+ $this->assertCount(2,$x->getObject('mail')->tagValues());
+ $this->assertCount(0,array_diff(['barts@email.com','secondmail@example.com'],$x->getObject('mail')->values->dot()->values()->toArray()));
+
+ $this->assertCount(1,$x->getObject('facsimiletelephonenumber')->values);
+ $this->assertCount(1,$x->getObject('facsimiletelephonenumber')->tagValues());
+ $this->assertCount(0,array_diff(['+1 555 222 4444'],$x->getObject('facsimiletelephonenumber')->values->dot()->values()->toArray()));
+ }
+
+ public function testLDIF_Import_Delete() {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.2.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertCount(1,$x->getObject('mail')->values);
+ $this->assertCount(2,$x->getObject('mail')->tagValues());
+ $this->assertCount(0,array_diff(['barts@email.com','secondmail@example.com'],$x->getObject('mail')->values->dot()->values()->toArray()));
+
+ $this->assertNull($x->getObject('facsimiletelephonenumber'));
+ }
+
+ public function testLDIF_Import_Append_Langtag() {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.3.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertCount(3,$x->getObject('mail')->values);
+ $this->assertCount(4,$x->getObject('mail')->values->dot());
+ $this->assertCount(2,$x->getObject('mail')->tagValues());
+ $this->assertCount(1,$x->getObject('mail')->tagValues('lang-au'));
+ $this->assertCount(1,$x->getObject('mail')->tagValues('lang-cn'));
+ $this->assertCount(0,array_diff(['barts@email.com','secondmail@example.com','au-email@example.com','cn-email@example.com'],$x->getObject('mail')->values->dot()->values()->toArray()));
+ }
+
+ public function testLDIF_Import_Replace_Langtag() {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.4.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertCount(3,$x->getObject('mail')->values);
+ $this->assertCount(4,$x->getObject('mail')->values->dot());
+ $this->assertCount(2,$x->getObject('mail')->tagValues());
+ $this->assertCount(1,$x->getObject('mail')->tagValues('lang-au'));
+ $this->assertCount(1,$x->getObject('mail')->tagValues('lang-cn'));
+ $this->assertCount(0,array_diff(['notag@example.com','notag1@example.com','au-tag@example.com','cn-tag@example.com'],$x->getObject('mail')->values->dot()->values()->toArray()));
+ }
+
+ public function testLDIF_Import_Add_Base64()
+ {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.5.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertEquals(3396,strlen($x->getObject('jpegphoto')->values->dot()->first()));
+ }
+
+ public function testLDIF_Import_Replace_Base64()
+ {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.6.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertEquals(3024,strlen($x->getObject('jpegphoto')->values->dot()->first()));
+ }
+
+ public function testLDIF_Import_Multi() {
+ $dn = 'cn=Bart Simpson,ou=People,o=Simpsons';
+ $import_file = __DIR__.'/data/ldif-import.7.ldif';
+
+ $this->assertTrue($this->login());
+
+ // Check that it exists
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+
+ $file = new UploadedFile($import_file,basename($import_file),null,null,true);
+
+ $response = $this
+ ->actingAs(Auth::user())
+ ->from('/import')
+ ->post('/import/process/ldif',[
+ '_token' => csrf_token(),
+ '_key'=>Crypt::encryptString('*import|_NOP'),
+ 'file' => $file,
+ ]);
+
+ $response->assertSuccessful();
+
+ // Check that it hsa been created
+ $this->assertEquals($dn,$x=config('server')->fetch($dn));
+ $this->assertTrue($x->exists);
+ $this->assertCount(4,$x->getObject('objectclass'));
+
+ $this->assertCount(3,$x->getObject('mail')->values);
+ $this->assertCount(6,$x->getObject('mail')->values->dot());
+ $this->assertCount(2,$x->getObject('mail')->tagValues());
+ $this->assertCount(2,$x->getObject('mail')->tagValues('lang-au'));
+ $this->assertCount(2,$x->getObject('mail')->tagValues('lang-cn'));
+ $this->assertCount(2,array_diff(['notag1@simpsons.example.com','notag2@simpsons.example.com','au-tag@simpsons.example.com','cn-tag@simpsons.example.com'],$x->getObject('mail')->values->dot()->values()->toArray()));
}
}
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.1.ldif b/tests/Feature/data/ldif-import.1.ldif
new file mode 100644
index 00000000..682b7475
--- /dev/null
+++ b/tests/Feature/data/ldif-import.1.ldif
@@ -0,0 +1,12 @@
+# Multiple actions
+version: 1
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype: modify
+replace: mail
+mail: barts@email.com
+-
+add: mail
+mail: secondmail@example.com
+-
+add: facsimiletelephonenumber
+facsimiletelephonenumber: +1 555 222 4444
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.2.ldif b/tests/Feature/data/ldif-import.2.ldif
new file mode 100644
index 00000000..0a8b1267
--- /dev/null
+++ b/tests/Feature/data/ldif-import.2.ldif
@@ -0,0 +1,5 @@
+# Delete actions
+version: 1
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype:modify
+delete: facsimiletelephonenumber
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.3.ldif b/tests/Feature/data/ldif-import.3.ldif
new file mode 100644
index 00000000..152550f4
--- /dev/null
+++ b/tests/Feature/data/ldif-import.3.ldif
@@ -0,0 +1,7 @@
+# Add lang-tag option with multiple values
+version: 1
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype: modify
+add: mail
+mail;lang-au: au-email@example.com
+mail;lang-cn: cn-email@example.com
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.4.ldif b/tests/Feature/data/ldif-import.4.ldif
new file mode 100644
index 00000000..4f0fd2d1
--- /dev/null
+++ b/tests/Feature/data/ldif-import.4.ldif
@@ -0,0 +1,9 @@
+# Multiple values with tags and replace
+version: 1
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype: modify
+replace: mail
+mail: notag@example.com
+mail: notag1@example.com
+mail;lang-au: au-tag@example.com
+mail;lang-cn: cn-tag@example.com
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.5.ldif b/tests/Feature/data/ldif-import.5.ldif
new file mode 100644
index 00000000..59c3f95c
--- /dev/null
+++ b/tests/Feature/data/ldif-import.5.ldif
@@ -0,0 +1,68 @@
+version: 1
+# Entry 1: cn=Bart Simpson,ou=People,o=Simpsons
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype:modify
+delete:jpegphoto
+-
+add:jpegphoto
+jpegPhoto:: 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
+ zt1Zj1J/wDtsTcHQxCJtll8Rr3105kOg4DkF//Z
diff --git a/tests/Feature/data/ldif-import.6.ldif b/tests/Feature/data/ldif-import.6.ldif
new file mode 100644
index 00000000..36260c5b
--- /dev/null
+++ b/tests/Feature/data/ldif-import.6.ldif
@@ -0,0 +1,61 @@
+version: 1
+# Entry 1: cn=Bart Simpson,ou=People,o=Simpsons
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype:modify
+delete:jpegphoto
+-
+replace:jpegphoto
+jpegPhoto:: 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
\ No newline at end of file
diff --git a/tests/Feature/data/ldif-import.7.ldif b/tests/Feature/data/ldif-import.7.ldif
new file mode 100644
index 00000000..8650d20a
--- /dev/null
+++ b/tests/Feature/data/ldif-import.7.ldif
@@ -0,0 +1,12 @@
+version: 1
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype: modify
+delete:mail
+
+dn: cn=Bart Simpson,ou=People,o=Simpsons
+changetype: modify
+add: mail
+mail: notag1@simpsons.example.com
+mail: notag2@simpsons.example.com
+mail;lang-au: au-email@simpsons.example.com
+mail;lang-cn: cn-email@simpsons.example.com
\ No newline at end of file