Compare commits

...

17 Commits

Author SHA1 Message Date
e4d2304696 Attribute is no longer iterable - can be used now that we manage attribute tags
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 3m32s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m43s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-05 23:24:55 +11:00
95abfc1625 Render HTML inputs for a DN with language tags - work for #16 2025-04-05 23:24:55 +11:00
5ef021c381 Display a DN entry with language tags - work for #16 2025-04-05 23:24:45 +11:00
0e2a8705ea Revert version to 2.1.0-dev 2025-04-05 23:24:45 +11:00
705bfb2d64 Update page_actions to be consistent with what we can do so far 2025-04-05 23:24:45 +11:00
3a3bf2addb Make select automatically selecting one item when there is only one configurable 2025-04-05 23:24:45 +11:00
5bb573100b Further to eab4f04 we need some attributes to render tree icons 2025-04-04 20:48:42 +11:00
a57ee78492 Ensure that Attribute::required() doesnt work with NULL $this->schema. Avoids issue as reported by #306 2025-04-04 20:48:42 +11:00
eab4f0427c No need to retrieve all records by default when getting children. By default sort records by DN until we implemented configurable sorting.
Should help the timeout issues reported in #301
2025-03-20 21:17:28 +11:00
fd2c5d1286 Add some attribute tags messages when we cant handle some attributes. 2025-03-19 09:41:47 +11:00
b35b44b2b8 Import and Export work with attribute tags 2025-03-19 09:41:47 +11:00
ce66dcb2b5 Remove deprecated Attribute::lang_tags 2025-03-19 09:41:47 +11:00
56a91f853c Fix export to work with no_attr_tags 2025-03-19 09:41:47 +11:00
81e0e58650 Handle no attribute tags at an Attribute::class level, added form/disabled components 2025-03-19 09:41:47 +11:00
1470170928 Internal attributes are now handled by the new backend setup for attribute tags 2025-03-19 09:41:47 +11:00
85c7132b30 Start of work to handle attribute tags - should help with #75 and #16 2025-03-19 09:41:47 +11:00
7e050954c3 Release v2.0.3
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-19 09:04:22 +11:00
49 changed files with 632 additions and 313 deletions

View File

@ -61,6 +61,7 @@ Support is known for these LDAP servers:
- [X] OpenLDAP
- [X] OpenDJ
- [ ] Microsoft Active Directory
- [ ] 389 Directory Server
If there is an LDAP server that you have that you would like to have supported, please open an issue to request it.
You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled

View File

@ -7,11 +7,12 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType;
use App\Ldap\Entry;
/**
* Represents an attribute of an LDAP Object
*/
class Attribute implements \Countable, \ArrayAccess, \Iterator
class Attribute implements \Countable, \ArrayAccess
{
// Attribute Name
protected string $name;
@ -19,24 +20,21 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// Is this attribute an internal attribute
protected(set) bool $is_internal = FALSE;
protected(set) bool $no_attr_tags = FALSE;
// MIN/MAX number of values
protected(set) int $min_values_count = 0;
protected(set) int $max_values_count = 0;
// RFC3866 Language Tags
/* @deprecated use $values/$values_old when playing with language tags */
protected Collection $lang_tags;
// The schema's representation of this attribute
protected(set) ?AttributeType $schema;
// The DN this object is in
protected(set) string $dn;
// The old values for this attribute - helps with isDirty() to determine if there is an update pending
protected(set) Collection $values_old;
private Collection $_values_old;
// Current Values
public Collection $values;
private Collection $_values;
// The objectclasses of the entry that has this attribute
protected(set) Collection $oc;
@ -103,11 +101,10 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
{
$this->dn = $dn;
$this->name = $name;
$this->values_old = collect($values);
$this->_values = collect($values);
$this->_values_old = collect($values);
$this->values = collect();
$this->oc = collect($oc);
$this->lang_tags = collect();
$this->schema = (new Server)
->schema('attributetypes',$name);
@ -152,59 +149,51 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
'required_by' => $this->schema?->required_by_object_classes ?: collect(),
// Used in Object Classes
'used_in' => $this->schema?->used_in_object_classes ?: collect(),
// The current attribute values
'values' => $this->no_attr_tags ? $this->tagValues() : $this->_values,
// The original attribute values
'values_old' => $this->no_attr_tags ? $this->tagValuesOld() : $this->_values_old,
default => throw new \Exception('Unknown key:' . $key),
};
}
public function __set(string $key,mixed $values): void
{
switch ($key) {
case 'values':
$this->_values = $values;
break;
case 'values_old':
$this->_values_old = $values;
break;
default:
throw new \Exception('Unknown key:'.$key);
}
}
public function __toString(): string
{
return $this->name;
}
public function addValue(string $value): void
{
$this->values->push($value);
}
public function current(): mixed
{
return $this->values->get($this->counter);
}
public function next(): void
{
$this->counter++;
}
public function key(): mixed
{
return $this->counter;
}
public function valid(): bool
{
return $this->values->has($this->counter);
}
public function rewind(): void
{
$this->counter = 0;
}
/* INTERFACE */
public function count(): int
{
return $this->values->count();
return $this->_values->dot()->count();
}
public function offsetExists(mixed $offset): bool
{
return ! is_null($this->values->has($offset));
return $this->_values->dot()->has($offset);
}
public function offsetGet(mixed $offset): mixed
{
return $this->values->get($offset);
return $this->_values->dot()->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
@ -217,6 +206,24 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// We cannot clear values using array syntax
}
/* METHODS */
public function addValue(string $tag,array $values): void
{
$this->_values->put(
$tag,
array_unique(array_merge($this->_values
->get($tag,[]),$values)));
}
public function addValueOld(string $tag,array $values): void
{
$this->_values_old->put(
$tag,
array_unique(array_merge($this->_values_old
->get($tag,[]),$values)));
}
/**
* Return the hints about this attribute, ie: RDN, Required, etc
*
@ -236,10 +243,6 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
if ($this->required()->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
// This attribute has language tags
if ($this->lang_tags->count())
$result->put(__('language tags'),sprintf('%s: %d',__('This Attribute has Language Tags'),$this->lang_tags->count()));
return $result->toArray();
}
@ -250,8 +253,10 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
*/
public function isDirty(): bool
{
return ($this->values_old->count() !== $this->values->count())
|| ($this->values->diff($this->values_old)->count() !== 0);
return (($a=$this->values_old->dot())->keys()->count() !== ($b=$this->values->dot())->keys()->count())
|| ($a->values()->count() !== $b->values()->count())
|| ($a->keys()->diff($b->keys())->count() !== 0)
|| ($a->values()->diff($b->values())->count() !== 0);
}
/**
@ -291,14 +296,14 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
->with('new',$new);
}
public function render_item_old(int $key): ?string
public function render_item_old(string $dotkey): ?string
{
return Arr::get($this->values_old,$key);
return Arr::get($this->values_old->dot(),$dotkey);
}
public function render_item_new(int $key): ?string
public function render_item_new(string $dotkey): ?string
{
return Arr::get($this->values,$key);
return Arr::get($this->values->dot(),$dotkey);
}
/**
@ -310,20 +315,21 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
{
// If we dont have any objectclasses then we cant know if it is required
return $this->oc->count()
? $this->oc->intersect($this->schema->required_by_object_classes->keys())->sort()
? $this->oc->intersect($this->required_by->keys())->sort()
: collect();
}
/**
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured
*
* @param string $tag
* @param array $value
* @return void
* @deprecated
*/
public function setLangTag(string $tag,array $value): void
public function tagValues(string $tag=Entry::TAG_NOTAG): Collection
{
$this->lang_tags->put($tag,$value);
return collect($this->_values
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
}
public function tagValuesOld(string $tag=Entry::TAG_NOTAG): Collection
{
return collect($this->_values_old
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
}
}

View File

@ -5,6 +5,7 @@ namespace App\Classes\LDAP\Attribute\Binary;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Binary;
use App\Ldap\Entry;
use App\Traits\MD5Updates;
/**
@ -14,13 +15,14 @@ final class JpegPhoto extends Binary
{
use MD5Updates;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG): View
{
return view('components.attribute.binary.jpegphoto')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('langtag',$langtag)
->with('f',new \finfo);
}
}

View File

@ -22,11 +22,17 @@ class Factory
public const map = [
'createtimestamp' => Internal\Timestamp::class,
'creatorsname' => Internal\DN::class,
'configcontext' => Schema\Generic::class,
'contextcsn' => Internal\CSN::class,
'entrycsn' => Internal\CSN::class,
'entrydn' => Internal\DN::class,
'entryuuid' => Internal\UUID::class,
'etag' => Internal\Etag::class,
'krblastfailedauth' => Attribute\NoAttrTags\Generic::class,
'krblastpwdchange' => Attribute\NoAttrTags\Generic::class,
'krblastsuccessfulauth' => Attribute\NoAttrTags\Generic::class,
'krbpasswordexpiration' => Attribute\NoAttrTags\Generic::class,
'krbloginfailedcount' => Attribute\NoAttrTags\Generic::class,
'krbprincipalkey' => KrbPrincipalKey::class,
'krbticketflags' => KrbTicketFlags::class,
'gidnumber' => GidNumber::class,
@ -34,6 +40,8 @@ class Factory
'jpegphoto' => Binary\JpegPhoto::class,
'modifytimestamp' => Internal\Timestamp::class,
'modifiersname' => Internal\DN::class,
'monitorcontext' => Schema\Generic::class,
'namingcontexts' => Schema\Generic::class,
'numsubordinates' => Internal\NumSubordinates::class,
'objectclass' => ObjectClass::class,
'pwdpolicysubentry' => Internal\PwdPolicySubentry::class,
@ -42,6 +50,7 @@ class Factory
'supportedcontrol' => Schema\OID::class,
'supportedextension' => Schema\OID::class,
'supportedfeatures' => Schema\OID::class,
'supportedldapversion' => Schema\Generic::class,
'supportedsaslmechanisms' => Schema\Mechanisms::class,
'userpassword' => Password::class,
];

View File

@ -9,4 +9,5 @@ use App\Classes\LDAP\Attribute;
*/
final class GidNumber extends Attribute
{
protected(set) bool $no_attr_tags = FALSE;
}

View File

@ -12,6 +12,7 @@ use App\Classes\LDAP\Attribute;
abstract class Internal extends Attribute
{
protected(set) bool $is_internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{

View File

@ -15,6 +15,8 @@ final class KrbPrincipalKey extends Attribute
{
use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.krbprincipalkey')
@ -24,18 +26,16 @@ final class KrbPrincipalKey extends Attribute
->with('new',$new);
}
public function render_item_old(int $key): ?string
public function render_item_old(string $dotkey): ?string
{
$pw = Arr::get($this->values_old,$key);
return $pw
return parent::render_item_old($dotkey)
? str_repeat('*',16)
: NULL;
}
public function render_item_new(int $key): ?string
public function render_item_new(string $dotkey): ?string
{
$pw = Arr::get($this->values,$key);
return $pw
return parent::render_item_new($dotkey)
? str_repeat('*',16)
: NULL;
}

View File

@ -3,9 +3,9 @@
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
use Illuminate\Support\Collection;
/**
* Represents an attribute whose value is a Kerberos Ticket Flag
@ -13,6 +13,8 @@ use Illuminate\Support\Collection;
*/
final class KrbTicketFlags extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
private const DISALLOW_POSTDATED = 0x00000001;
private const DISALLOW_FORWARDABLE = 0x00000002;
private const DISALLOW_TGT_BASED = 0x00000004;

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\NoAttrTags;
use App\Classes\LDAP\Attribute;
/**
* Represents an Attribute that doesnt have Lang Tags
*/
class Generic extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
}

View File

@ -12,6 +12,8 @@ use App\Classes\LDAP\Attribute;
*/
final class ObjectClass extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
// The schema ObjectClasses for this objectclass of a DN
protected Collection $oc_schema;
@ -21,25 +23,39 @@ final class ObjectClass extends Attribute
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has
* @param array $oc The objectclasses that the DN of this attribute has (ignored for objectclasses)
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
{
parent::__construct($dn,$name,$values,['top']);
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$this->values->merge($this->values_old)->unique()->contains($item->name));
$this->set_oc_schema($this->tagValuesOld());
}
public function __get(string $key): mixed
{
return match ($key) {
'structural' => $this->oc_schema->filter(fn($item) => $item->isStructural()),
'structural' => $this->oc_schema->filter(fn($item)=>$item->isStructural()),
default => parent::__get($key),
};
}
public function __set(string $key,mixed $values): void
{
switch ($key) {
case 'values':
parent::__set($key,$values);
// We need to populate oc_schema, if we are a new OC and thus dont have any old values
if (! $this->values_old->count() && $this->values->count())
$this->set_oc_schema($this->tagValues());
break;
default: parent::__set($key,$values);
}
}
/**
* Is a specific value the structural objectclass
*
@ -61,4 +77,11 @@ final class ObjectClass extends Attribute
->with('old',$old)
->with('new',$new);
}
private function set_oc_schema(Collection $tv): void
{
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$tv->contains($item->name));
}
}

View File

@ -15,6 +15,9 @@ use App\Traits\MD5Updates;
final class Password extends Attribute
{
use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
private const password_helpers = 'Classes/LDAP/Attribute/Password';
public const commands = 'App\\Classes\\LDAP\\Attribute\\Password\\';
@ -85,19 +88,23 @@ final class Password extends Attribute
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort());
}
public function render_item_old(int $key): ?string
public function render_item_old(string $dotkey): ?string
{
$pw = Arr::get($this->values_old,$key);
$pw = parent::render_item_old($dotkey);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
? (((($x=$this->hash($pw)) && ($x::id() === '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
: NULL;
}
public function render_item_new(int $key): ?string
public function render_item_new(string $dotkey): ?string
{
$pw = Arr::get($this->values,$key);
$pw = parent::render_item_new($dotkey);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
: NULL;
}
}

View File

@ -14,6 +14,7 @@ use App\Classes\LDAP\Attribute;
abstract class Schema extends Attribute
{
protected bool $internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
protected static function _get(string $filename,string $string,string $key): ?string
{
@ -30,7 +31,7 @@ abstract class Schema extends Attribute
while (! feof($f)) {
$line = trim(fgets($f));
if (! $line OR preg_match('/^#/',$line))
if ((! $line) || preg_match('/^#/',$line))
continue;
$fields = explode(':',$line);
@ -41,12 +42,15 @@ abstract class Schema extends Attribute
'desc'=>Arr::get($fields,3,__('No description available, can you help with one?')),
]);
}
fclose($f);
return $result;
});
return Arr::get(($array ? $array->get($string) : []),$key);
return Arr::get(($array ? $array->get($string) : []),
$key,
__('No description available, can you help with one?'));
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View

View File

@ -0,0 +1,20 @@
<?php
namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema;
/**
* Represents a Generic Schema Attribute
*/
class Generic extends Schema
{
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.schema.generic')
->with('o',$this);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Classes\LDAP\Export;
use Illuminate\Support\Str;
use App\Classes\LDAP\Export;
use App\Ldap\Entry;
/**
* Export from LDAP using an LDIF format
@ -41,13 +42,25 @@ class LDIF extends Export
// Display Attributes
foreach ($o->getObjects() as $ao) {
foreach ($ao->values as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name,$value)
: sprintf('%s:: %s',$ao->name,base64_encode($value))
,$this->br);
}
if ($ao->no_attr_tags)
foreach ($ao->values as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name,$value)
: sprintf('%s:: %s',$ao->name,base64_encode($value))
,$this->br);
}
else
foreach ($ao->values as $tag => $tagvalues) {
foreach ($tagvalues as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),$value)
: sprintf('%s:: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),base64_encode($value))
,$this->br);
}
}
}
}

View File

@ -59,8 +59,6 @@ class LDIF extends Import
$base64encoded = FALSE;
$attribute = NULL;
$value = '';
// Else its a blank line
}
continue;
@ -69,7 +67,7 @@ class LDIF extends Import
$m = [];
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
switch ($x=Arr::get($m,1)) {
switch (Arr::get($m,1)) {
case 'changetype':
if ($m[2] !== ':')
throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
@ -133,7 +131,6 @@ class LDIF extends Import
// Start of a new attribute
$base64encoded = ($m[2] === '::');
// @todo Need to parse attributes with ';' options
$attribute = $m[1];
$value = $m[3];
@ -160,7 +157,7 @@ class LDIF extends Import
}
public function readEntry() {
static $haveVersion = false;
static $haveVersion = FALSE;
if ($lines = $this->nextLines()) {
@ -179,7 +176,7 @@ class LDIF extends Import
} else
$changetype = 'add';
$this->template = new Template($this->server_id,null,null,$changetype);
$this->template = new Template($this->server_id,NULL,NULL,$changetype);
switch ($changetype) {
case 'add':
@ -201,7 +198,7 @@ class LDIF extends Import
return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines);
$this->template->setDN($dn);
$this->template->accept(false,true);
$this->template->accept(FALSE,TRUE);
return $this->getModifyDetails($lines);
@ -228,6 +225,6 @@ class LDIF extends Import
return $this->error(_('A valid dn line is required'),$lines);
} else
return false;
return FALSE;
}
}

View File

@ -209,7 +209,8 @@ final class Server
/**
* Obtain the rootDSE for the server, that gives us server information
*
* @param null $connection
* @param string|null $connection
* @param Carbon|null $cachetime
* @return Entry|null
* @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE();
@ -230,7 +231,7 @@ final class Server
/**
* Get the Schema DN
*
* @param $connection
* @param string|null $connection
* @return string
* @throws ObjectNotFoundException
*/
@ -245,16 +246,21 @@ final class Server
* Query the server for a DN and return its children and if those children have children.
*
* @param string $dn
* @param array $attrs
* @return LDAPCollection|NULL
*/
public function children(string $dn): ?LDAPCollection
public function children(string $dn,array $attrs=['dn']): ?LDAPCollection
{
return ($x=(new Entry)
->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select(['*','hassubordinates'])
->select(array_merge($attrs,[
'hassubordinates', // Needed for the tree to know if an entry has children
'c' // Needed for the tree to show icons for countries
]))
->setDn($dn)
->list()
->orderBy('dn')
->get()) ? $x : NULL;
}
@ -528,7 +534,6 @@ final class Server
*
* @param string $oid
* @return LDAPSyntax|null
* @throws InvalidUsage
*/
public function schemaSyntaxName(string $oid): ?LDAPSyntax
{

View File

@ -58,10 +58,10 @@ class HomeController extends Controller
$o = new Entry;
if (count(array_filter($x=old('objectclass',$request->objectclass)))) {
$o->objectclass = $x;
$o->objectclass = [Entry::TAG_NOTAG=>$x];
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->{$ao->name} = '';
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
$o->setRDNBase($key['dn']);
}
@ -188,8 +188,6 @@ class HomeController extends Controller
$result = (new Entry)
->query()
//->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
//->select(['*'])
->setDn($dn)
->recursive()
->get();
@ -269,19 +267,22 @@ class HomeController extends Controller
// We need to process and encrypt the password
if ($request->userpassword) {
$passwords = [];
foreach ($request->userpassword as $key => $value) {
$po = $o->getObject('userpassword');
foreach (Arr::dot($request->userpassword) as $dotkey => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) {
array_push($passwords,$old);
if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
$passwords[$dotkey] = $value;
continue;
}
if ($value) {
$type = Arr::get($request->userpassword_hash,$key);
array_push($passwords,Password::hash_id($type)->encode($value));
$type = Arr::get($request->userpassword_hash,$dotkey);
$passwords[$dotkey] = Password::hash_id($type)
->encode($value);
}
}
$o->userpassword = $passwords;
$o->userpassword = Arr::undot($passwords);
}
if (! $o->getDirty())
@ -386,7 +387,13 @@ class HomeController extends Controller
'dn' => $view
->with('dn',$key['dn'])
->with('o',$o)
->with('page_actions',collect(['edit'=>TRUE])),
->with('page_actions',collect([
'copy'=>FALSE,
'create'=>FALSE,
'delete'=>TRUE,
'edit'=>TRUE,
'export'=>TRUE,
])),
'import' => $view,

View File

@ -14,8 +14,17 @@ use App\Classes\LDAP\Export\LDIF;
use App\Exceptions\Import\AttributeException;
use App\Exceptions\InvalidUsage;
/**
* An Entry in an LDAP server
*
* @notes https://ldap.com/ldap-dns-and-rdns
*/
class Entry extends Model
{
private const TAG_CHARS = 'a-zA-Z0-9-';
private const TAG_CHARS_LANG = 'lang-['.self::TAG_CHARS.']';
public const TAG_NOTAG = '_null_';
// Our Attribute objects
private Collection $objects;
/* @deprecated */
@ -45,13 +54,18 @@ class Entry extends Model
/**
* This function overrides getAttributes to use our collection of Attribute objects instead of the models attributes.
*
* This returns an array that should be consistent with $this->attributes
*
* @return array
* @note $this->attributes may not be updated with changes
*/
public function getAttributes(): array
{
return $this->objects
->map(fn($item)=>$item->values)
->flatMap(fn($item)=>
($item->no_attr_tags)
? [strtolower($item->name)=>$item->values]
: $item->values
->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v]))
->toArray();
}
@ -64,12 +78,10 @@ class Entry extends Model
{
$key = $this->normalizeAttributeKey($key);
// @todo Silently ignore keys of language tags - we should work with them
if (str_contains($key,';'))
return TRUE;
list($attribute,$tag) = $this->keytag($key);
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($key)))
|| (! $this->getObject($key)->isDirty());
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($attribute)))
|| (! $this->getObject($attribute)->isDirty());
}
public static function query(bool $noattrs=false): Builder
@ -86,18 +98,22 @@ class Entry extends Model
* As attribute values are updated, or new ones created, we need to mirror that
* into our $objects. This is called when we $o->key = $value
*
* This function should update $this->attributes and correctly reflect changes in $this->objects
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function setAttribute(string $key,mixed $value): static
{
parent::setAttribute($key,$value);
foreach ($value as $k => $v)
parent::setAttribute($key.($k !== self::TAG_NOTAG ? ';'.$k : ''),$v);
$key = $this->normalizeAttributeKey($key);
list($attribute,$tags) = $this->keytag($key);
$o = $this->objects->get($key) ?: Factory::create($this->dn ?: '',$key,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($this->attributes[$key]);
$o = $this->objects->get($attribute) ?: Factory::create($this->dn ?: '',$attribute,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($value);
$this->objects->put($key,$o);
@ -158,15 +174,18 @@ class Entry extends Model
if (! is_string($value))
throw new \Exception('value should be a string');
$key = $this->normalizeAttributeKey($key);
$key = $this->normalizeAttributeKey(strtolower($key));
if (! config('server')->schema('attributetypes')->has($key))
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$key));
// If the attribute name has tags
list($attribute,$tag) = $this->keytag($key);
$o = $this->objects->get($key) ?: Attribute\Factory::create($this->dn ?: '',$key,[]);
$o->addValue($value);
if (! config('server')->schema('attributetypes')->has($attribute))
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
$this->objects->put($key,$o);
$o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]);
$o->addValue($tag,[$value]);
$this->objects->put($attribute,$o);
}
/**
@ -179,34 +198,26 @@ class Entry extends Model
$result = collect();
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
foreach ($this->attributes as $attribute => $values) {
// If the attribute name has tags
$matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
foreach ($this->attributes as $attrtag => $values) {
list($attribute,$tags) = $this->keytag($attrtag);
// If the attribute doesnt exist we'll create it
$o = Arr::get(
$result,
$orig = Arr::get($this->original,$attrtag,[]);
// If the attribute doesnt exist we'll create it
$o = Arr::get(
$result,
$attribute,
Factory::create(
$this->dn,
$attribute,
Factory::create(
$this->dn,
$attribute,
Arr::get($this->original,$attribute,[]),
$entry_oc,
));
$o->setLangTag($matches[3],$values);
[$tags=>$orig],
$entry_oc,
));
} else {
$o = Factory::create($this->dn,$attribute,Arr::get($this->original,$attribute,[]),$entry_oc);
}
$o->addValue($tags,$values);
$o->addValueOld($tags,Arr::get($this->original,$attrtag));
if (! $result->has($attribute)) {
// Store our new values to know if this attribute has changed
$o->values = collect($values);
$result->put($attribute,$o);
}
$result->put($attribute,$o);
}
$sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item));
@ -247,7 +258,7 @@ class Entry extends Model
{
$result = collect();
foreach ($this->objectclass as $oc)
foreach ($this->getObject('objectclass')->values as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes);
return $result;
@ -274,6 +285,38 @@ class Entry extends Model
->filter(fn($item)=>$item->is_internal);
}
/**
* Identify the language tags (RFC 3866) used by this entry
*
* @return Collection
*/
public function getLangTags(): Collection
{
return $this->getObjects()
->filter(fn($item)=>! $item->no_attr_tags)
->map(fn($item)=>$item
->values
->keys()
->filter(fn($item)=>preg_match(sprintf('/%s+;?/',self::TAG_CHARS_LANG),$item))
->map(fn($item)=>preg_replace('/lang-/','',$item))
)
->filter(fn($item)=>$item->count());
}
/**
* Of all the items with lang tags, which ones have more than 1 lang tag
*
* @return Collection
*/
public function getLangMultiTags(): Collection
{
return $this->getLangTags()
->map(fn($item)=>$item->values()
->map(fn($item)=>explode(';',$item))
->filter(fn($item)=>count($item) > 1))
->filter(fn($item)=>$item->count());
}
/**
* Get an attribute as an object
*
@ -299,10 +342,37 @@ class Entry extends Model
return $this->objects;
}
/**
* Find other attribute tags used by this entry
*
* @return Collection
*/
public function getOtherTags(): Collection
{
return $this->getObjects()
->filter(fn($item)=>! $item->no_attr_tags)
->map(fn($item)=>$item
->values
->keys()
->filter(fn($item)=>
$item && collect(explode(';',$item))->filter(
fn($item)=>
(! preg_match(sprintf('/^%s$/',self::TAG_NOTAG),$item))
&& (! preg_match(sprintf('/^%s+$/',self::TAG_CHARS_LANG),$item))
&& (! preg_match('/^binary$/',$item))
)
->count())
)
->filter(fn($item)=>$item->count());
}
/**
* Return a list of attributes without any values
*
* @return Collection
* @todo Dont show attributes that are not provided by an objectclass, make a new function to show those
* This is for dynamic list items eg: labeledURI, which are not editable.
* We can highlight those values that are as a result of a dynamic module
*/
public function getMissingAttributes(): Collection
{
@ -323,12 +393,14 @@ class Entry extends Model
/**
* Return this list of user attributes
*
* @param string|null $tag If null return all tags
* @return Collection
*/
public function getVisibleAttributes(): Collection
public function getVisibleAttributes(?string $tag=NULL): Collection
{
return $this->objects
->filter(fn($item)=>! $item->is_internal);
->filter(fn($item)=>! $item->is_internal)
->filter(fn($item)=>is_null($tag) || count($item->tagValues($tag)) > 0);
}
public function hasAttribute(int|string $key): bool
@ -374,65 +446,92 @@ class Entry extends Model
*/
public function icon(): string
{
$objectclasses = array_map('strtolower',$this->objectclass);
$objectclasses = $this->getObject('objectclass')
->tagValues()
->map(fn($item)=>strtolower($item));
// Return icon based upon objectClass value
if (in_array('person',$objectclasses) ||
in_array('organizationalperson',$objectclasses) ||
in_array('inetorgperson',$objectclasses) ||
in_array('account',$objectclasses) ||
in_array('posixaccount',$objectclasses))
if ($objectclasses->intersect([
'account',
'inetorgperson',
'organizationalperson',
'person',
'posixaccount',
])->count())
return 'fas fa-user';
elseif (in_array('organization',$objectclasses))
elseif ($objectclasses->contains('organization'))
return 'fas fa-university';
elseif (in_array('organizationalunit',$objectclasses))
elseif ($objectclasses->contains('organizationalunit'))
return 'fas fa-object-group';
elseif (in_array('posixgroup',$objectclasses) ||
in_array('groupofnames',$objectclasses) ||
in_array('groupofuniquenames',$objectclasses) ||
in_array('group',$objectclasses))
elseif ($objectclasses->intersect([
'posixgroup',
'groupofnames',
'groupofuniquenames',
'group',
])->count())
return 'fas fa-users';
elseif (in_array('dcobject',$objectclasses) ||
in_array('domainrelatedobject',$objectclasses) ||
in_array('domain',$objectclasses) ||
in_array('builtindomain',$objectclasses))
elseif ($objectclasses->intersect([
'dcobject',
'domainrelatedobject',
'domain',
'builtindomain',
])->count())
return 'fas fa-network-wired';
elseif (in_array('alias',$objectclasses))
elseif ($objectclasses->contains('alias'))
return 'fas fa-theater-masks';
elseif (in_array('country',$objectclasses))
return sprintf('flag %s',strtolower(Arr::get($this->c,0)));
elseif ($objectclasses->contains('country'))
return sprintf('flag %s',strtolower(Arr::get($this->c ?: [],0)));
elseif (in_array('device',$objectclasses))
elseif ($objectclasses->contains('device'))
return 'fas fa-mobile-alt';
elseif (in_array('document',$objectclasses))
elseif ($objectclasses->contains('document'))
return 'fas fa-file-alt';
elseif (in_array('iphost',$objectclasses))
elseif ($objectclasses->contains('iphost'))
return 'fas fa-wifi';
elseif (in_array('room',$objectclasses))
elseif ($objectclasses->contains('room'))
return 'fas fa-door-open';
elseif (in_array('server',$objectclasses))
elseif ($objectclasses->contains('server'))
return 'fas fa-server';
elseif (in_array('openldaprootdse',$objectclasses))
elseif ($objectclasses->contains('openldaprootdse'))
return 'fas fa-info';
// Default
return 'fa-fw fas fa-cog';
}
/**
* Given an LDAP attribute, this will return the attribute name and the tag
* eg: description;lang-cn will return [description,lang-cn]
*
* @param string $key
* @return array
*/
private function keytag(string $key): array
{
$matches = [];
if (preg_match(sprintf('/^([%s]+);+([%s;]+)/',self::TAG_CHARS,self::TAG_CHARS),$key,$matches)) {
$attribute = $matches[1];
$tags = $matches[2];
} else {
$attribute = $key;
$tags = self::TAG_NOTAG;
}
return [$attribute,$tags];
}
/**
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class
*

View File

@ -11,8 +11,8 @@ trait MD5Updates
{
public function isDirty(): bool
{
foreach ($this->values->diff($this->values_old) as $key => $value)
if (md5(Arr::get($this->values_old,$key)) !== $value)
foreach ($this->values_old->dot()->keys()->merge($this->values->dot()->keys())->unique() as $dotkey)
if (md5(Arr::get($this->values_old->dot(),$dotkey)) !== Arr::get($this->values->dot(),$dotkey))
return TRUE;
return FALSE;

View File

@ -5,6 +5,7 @@ namespace App\View\Components;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Ldap\Entry;
class Attribute extends Component
{
@ -12,30 +13,32 @@ class Attribute extends Component
public bool $edit;
public bool $new;
public bool $old;
public ?string $na;
public string $langtag;
public ?string $na; // Text to render if the LDAPAttribute is null
/**
* Create a new component instance.
*/
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?string $na=NULL)
{
/**
* Create a new component instance.
*/
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG,?string $na=NULL)
{
$this->o = $o;
$this->edit = $edit;
$this->old = $old;
$this->new = $new;
$this->langtag = $langtag;
$this->na = $na;
}
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return $this->o
? $this->o
->render($this->edit,$this->old,$this->new)
: $this->na;
}
}
}

View File

@ -4,23 +4,25 @@ namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Ldap\Entry;
class AttributeType extends Component
{
private LDAPAttribute $o;
private bool $new;
private string $langtag;
/**
* Create a new component instance.
*/
public function __construct(LDAPAttribute $o,bool $new=FALSE)
public function __construct(LDAPAttribute $o,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG)
{
$this->o = $o;
$this->new = $new;
$this->langtag = $langtag;
}
/**
@ -30,6 +32,7 @@ class AttributeType extends Component
{
return view('components.attribute-type')
->with('o',$this->o)
->with('new',$this->new);
->with('new',$this->new)
->with('langtag',$this->langtag);
}
}

View File

@ -1 +1 @@
v2.0.2-rel
v2.1.0-dev

View File

@ -30,7 +30,10 @@ input.form-control.input-group-end {
.custom-tooltip-danger {
--bs-tooltip-bg: var(--bs-danger);
}
.custom-tooltip {
--bs-tooltip-bg: var(--bs-gray-900);
}
.tooltip {

View File

@ -0,0 +1,9 @@
<div class="alert alert-danger p-0" style="font-size: .80em;">
<table class="table table-borderless table-danger p-0 m-0">
<tr>
<td class="align-top" style="width: 5%;"><i class="fas fa-fw fa-2x fa-exclamation-triangle"></i></td>
<td>Unable to display this attribute as it has attribute tags [<strong>{!! $tags->join('</strong>, <strong>') !!}</strong>].<br>
You can manage it with an LDIF import.</td>
</tr>
</table>
</div>

View File

@ -15,7 +15,7 @@
</div>
</div>
<x-attribute :o="$o" :edit="true" :new="$new ?? FALSE"/>
<x-attribute :o="$o" :edit="true" :new="$new ?? FALSE" :langtag="$langtag"/>
</div>
</div>

View File

@ -1,19 +1,21 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $value)
@if (($edit ?? FALSE) && ! $o->is_rdn)
<div class="input-group has-validation">
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>($o->values->contains($value))]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))>
<div class="col-12">
@foreach(Arr::get(old($o->name_lc,[$langtag=>($new ?? FALSE) ? [NULL] : $o->tagValues($langtag)]),$langtag) as $key => $value)
@if(($edit ?? FALSE) && ! $o->is_rdn)
<div class="input-group has-validation">
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! ($tv=$o->tagValuesOld($langtag))->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))>
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
</div>
@else
{{ $value }}
@endif
@endforeach
@else
<input type="text" class="form-control mb-1" value="{{ $value }}" disabled>
@endif
@endforeach
</div>
</x-attribute.layout>

View File

@ -1,17 +1,17 @@
<!-- @todo We are not handling redirect backs yet with updated photos -->
<!-- $o=Binary\JpegPhoto::class -->
<x-attribute.layout :edit="$edit" :new="false" :o="$o">
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
<table class="table table-borderless p-0 m-0">
@foreach ($o->values_old as $value)
@foreach($o->tagValuesOld() as $key => $value)
<tr>
@switch ($x=$f->buffer($value,FILEINFO_MIME_TYPE))
@switch($x=$f->buffer($value,FILEINFO_MIME_TYPE))
@case('image/jpeg')
@default
<td>
<input type="hidden" name="{{ $o->name_lc }}[]" value="{{ md5($value) }}">
<img @class(['border','rounded','p-2','m-0','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index))]) src="data:{{ $x }};base64, {{ base64_encode($value) }}" />
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}">
<img alt="{{ $o->dn }}" @class(['border','rounded','p-2','m-0','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index))]) src="data:{{ $x }};base64, {{ base64_encode($value) }}" />
@if ($edit)
@if($edit)
<br>
<!-- @todo TO IMPLEMENT -->
<button class="btn btn-sm btn-danger deletable d-none mt-3" disabled><i class="fas fa-trash-alt"></i> @lang('Delete')</button>

View File

@ -1,5 +1,5 @@
<!-- $o=Internal::class -->
@foreach (old($o->name_lc,$o->values) as $value)
@foreach(old($o->name_lc,$o->values) as $value)
@if($loop->index)<br>@endif
{{ $value }}
@endforeach

View File

@ -1,5 +1,5 @@
<!-- $o=Internal\Timestamp::class -->
@foreach (old($o->name_lc,$o->values) as $value)
@foreach(old($o->name_lc,$o->values) as $value)
@if($loop->index)<br>@endif
{{ \Carbon\Carbon::createFromTimestamp(strtotime($value))->format(config('pla.datetime_format','Y-m-d H:i:s')) }}
@endforeach

View File

@ -1,8 +1,2 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="false" :new="false" :detail="true" :o="$o">
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $value)
<div class="input-group">
<input type="text" @class(['form-control','mb-1']) name="{{ $o->name_lc }}[]" value="{{ \Carbon\Carbon::createFromTimestamp(strtotime($value))->format(config('pla.datetime_format','Y-m-d H:i:s')) }}" @disabled(true)>
</div>
@endforeach
</x-attribute.layout>
<!-- $o=NoAttrTags/Generic::class -->
@include('components.form.disabled.datetime')

View File

@ -1 +1,2 @@
@include('components.attribute.krblastfailedauth')
<!-- $o=NoAttrTags/Generic::class -->
@include('components.form.disabled.datetime')

View File

@ -1 +1,2 @@
@include('components.attribute.krblastfailedauth')
<!-- $o=NoAttrTags/Generic::class -->
@include('components.form.disabled.datetime')

View File

@ -1,8 +1,2 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="false" :new="false" :detail="true" :o="$o">
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $value)
<div class="input-group">
<input type="text" @class(['form-control','mb-1']) name="{{ $o->name_lc }}[]" value="{{ $value }}" @disabled(true)>
</div>
@endforeach
</x-attribute.layout>
<!-- $o=NoAttrTags/Generic::class -->
@include('components.form.disabled.input')

View File

@ -1 +1,2 @@
@include('components.attribute.krblastfailedauth')
<!-- $o=NoAttrTags/Generic::class -->
@include('components.form.disabled.datetime')

View File

@ -1,10 +1,10 @@
<!-- @todo We are not handling redirect backs yet with updated passwords -->
<!-- $o=Password::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach($o->values_old as $value)
<!-- $o=KrbPrincipleKey::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
@foreach($o->tagValuesOld($langtag) as $key => $value)
@if($edit)
<div class="input-group has-validation mb-3">
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ md5($value) }}" @readonly(true)>
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
@ -13,7 +13,7 @@
</div>
</div>
@else
{{ str_repeat('*',16) }}
{{ $o->render_item_old($langtag.'.'.$key) }}
@endif
@endforeach
</x-attribute.layout>

View File

@ -1,21 +1,21 @@
<!-- $o=KrbTicketFlags::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach(($o->values->count() ? $o->values : ($new ? [0] : NULL)) as $value)
@foreach(Arr::get(old($o->name_lc,[$langtag=>$o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@if($edit)
<div id="32"></div>
<div id="16"></div>
<div class="input-group has-validation mb-3">
<input type="hidden" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ $value }}" @readonly(true)>
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
@if($e=$errors->get($o->name_lc.'.'.$loop->index))
{{ join('|',$e) }}
@endif
</div>
</div>
@else
{{ $value }}
{{ $o->render_item_old($langtag.'.'.$key) }}
@endif
@endforeach
</x-attribute.layout>

View File

@ -1,12 +1,12 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach(old($o->name_lc,$o->values) as $value)
@if ($edit)
<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value"/>
<!-- $o=Attribute/ObjectClass::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
@foreach(Arr::get(old($o->name_lc,[$langtag=>($new ?? FALSE) ? [NULL] : $o->tagValues($langtag)]),$langtag) as $key => $value)
@if($edit)
<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value" :langtag="$langtag"/>
@else
{{ $value }}
{{ $o->render_item_old($langtag.'.'.$key) }}
@if ($o->isStructural($value))
<input type="hidden" name="{{ $o->name_lc }}[]" value="{{ $value }}">
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}">
<span class="float-end">@lang('structural')</span>
@endif
<br>

View File

@ -1,11 +1,11 @@
<!-- @todo We are not handling redirect backs yet with updated passwords -->
<!-- $o=Password::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach($o->values_old as $value)
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
@foreach($o->tagValuesOld($langtag) as $key => $value)
@if($edit)
<div class="input-group has-validation mb-3">
<x-form.select id="userpassword_hash_{{$loop->index}}" name="userpassword_hash[]" :value="$o->hash($value)->id()" :options="$helpers" allowclear="false" :disabled="true"/>
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ md5($value) }}" @readonly(true)>
<x-form.select id="userpassword_hash_{{$loop->index}}" name="userpassword_hash[{{ $langtag }}][]" :value="$o->hash($value)->id()" :options="$helpers" allowclear="false" :disabled="true"/>
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
@ -14,7 +14,7 @@
</div>
</div>
@else
{{ (($x=$o->hash($value)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '' }}{{ str_repeat('*',16) }}
{{ $o->render_item_old($langtag.'.'.$key) }}
@endif
@endforeach
</x-attribute.layout>

View File

@ -0,0 +1 @@
{!! $o->values->join('<br>') !!}

View File

@ -1,9 +1,9 @@
<span id="objectclass_{{$value}}">
<div class="input-group has-validation">
<!-- @todo Have an "x" to remove the entry, we need an event to process the removal, removing any attribute values along the way -->
<input type="text" @class(['form-control','input-group-end','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)>
<input type="text" @class(['form-control','input-group-end','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)>
@if ($o->isStructural($value))
<span class="input-group-end text-black-50">structural</span>
<span class="input-group-end text-black-50">@lang('structural')</span>
@else
<span class="input-group-end"><i class="fas fa-fw fa-xmark"></i></span>
@endif

View File

@ -3,8 +3,7 @@
@php($clone=FALSE)
<span class="p-0 m-0">
@if($o->is_rdn)
<br/>
<span class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
<button class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</button>
@elseif($edit && $o->can_addvalues)
@switch(get_class($o))
@case(JpegPhoto::class)
@ -229,8 +228,11 @@
// Create a new entry when Add Value clicked
$('#{{ $o->name }}-addnew.addable').click(function (item) {
var cln = $(this).parent().parent().find('input:last').parent().clone();
cln.find('input:last').attr('value','').attr('placeholder', '[@lang('NEW')]');
cln.appendTo('#'+item.currentTarget.id.replace('-addnew',''));
cln.find('input:last')
.attr('value','')
.attr('placeholder', '[@lang('NEW')]')
.addClass('border-focus')
.appendTo('#'+item.currentTarget.id.replace('-addnew',''));
});
});
</script>

View File

@ -0,0 +1,8 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="false" :new="false" :detail="true" :o="$o">
@foreach(Arr::get(old($o->name_lc,[$langtag=>$o->tagValues($langtag)]),$langtag,[]) as $value)
<div class="input-group">
<input type="text" class="form-control mb-1" value="{{ \Carbon\Carbon::createFromTimestamp(strtotime($value))->format(config('pla.datetime_format','Y-m-d H:i:s')) }}" disabled>
</div>
@endforeach
</x-attribute.layout>

View File

@ -0,0 +1,8 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="false" :new="false" :detail="true" :o="$o">
@foreach(Arr::get(old($o->name_lc,[$langtag=>$o->tagValues($langtag)]),$langtag,[]) as $value)
<div class="input-group">
<input type="text" class="form-control mb-1" value="{{ $value }}" disabled>
</div>
@endforeach
</x-attribute.layout>

View File

@ -66,7 +66,7 @@
@endif
@isset($options)
@if($options->count() === 1)
@if(($autoselect ?? FALSE) && $options->count() === 1)
$('#{{ $id ?? $name }}')
.val('{{ $options->first()['id'] }}')
.trigger("change")

View File

@ -26,6 +26,12 @@
<x-attribute :o="$o->getObject('entryuuid')" :na="__('Unknown')"/>
</th>
</tr>
@if($langtags->count())
<tr class="mt-1">
<td class="p-0 pe-2">Tags</td>
<th class="p-0">{{ $langtags->join(', ') }}</th>
</tr>
@endif
</table>
</td>
</tr>

View File

@ -1,5 +1,4 @@
<div class="row">
<div class="col-12 col-xl-3">
<select id="attributetype" class="form-control">
<option value="-all-">-all-</option>

View File

@ -1,7 +1,10 @@
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header',['o'=>($oo=$server->fetch(old('container',$container)))])
@include('fragment.dn.header',[
'o'=>($oo=$server->fetch(old('container',$container))),
'langtags'=>collect(),
])
@endsection
@section('main-content')

View File

@ -1,7 +1,15 @@
@use(App\Ldap\Entry)
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header',['o'=>($o ?? $o=$server->fetch($dn))])
@include('fragment.dn.header',[
'o'=>($o ?? $o=$server->fetch($dn)),
'langtags'=>($langtags=$o->getLangTags()
->flatMap(fn($item)=>$item->values())
->unique()
->sort())
])
@endsection
@section('page_actions')
@ -9,25 +17,30 @@
<div class="col">
<div class="action-buttons float-end">
<ul class="nav">
@if(isset($page_actions) && $page_actions->contains('export'))
@if(isset($page_actions) && $page_actions->get('create'))
<li>
<button class="btn btn-outline-dark p-1 m-1" id="entry-copy-move" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('New Child')" disabled><i class="fas fa-fw fa-diagram-project fs-5"></i></button>
</li>
@endif
@if(isset($page_actions) && $page_actions->get('export'))
<li>
<span id="entry-export" data-bs-toggle="modal" data-bs-target="#page-modal">
<button class="btn btn-outline-dark p-1 m-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Export')"><i class="fas fa-fw fa-download fs-5"></i></button>
</span>
</li>
@endif
@if(isset($page_actions) && $page_actions->contains('copy'))
@if(isset($page_actions) && $page_actions->get('copy'))
<li>
<button class="btn btn-outline-dark p-1 m-1" id="entry-copy-move" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Copy/Move')" disabled><i class="fas fa-fw fa-copy fs-5"></i></button>
</li>
@endif
@if((isset($page_actions) && $page_actions->contains('edit')) || old())
@if(isset($page_actions) && $page_actions->get('edit'))
<li>
<button class="btn btn-outline-dark p-1 m-1" id="entry-edit" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Edit Entry')"><i class="fas fa-fw fa-edit fs-5"></i></button>
</li>
@endif
<!-- @todo Dont offer the delete button for an entry with children -->
@if(isset($page_actions) && $page_actions->contains('delete'))
@if(isset($page_actions) && $page_actions->get('delete'))
<li>
<span id="entry-delete" data-bs-toggle="modal" data-bs-target="#page-modal">
<button class="btn btn-outline-danger p-1 m-1" data-bs-custom-class="custom-tooltip-danger" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Delete Entry')"><i class="fas fa-fw fa-trash-can fs-5"></i></button>
@ -38,6 +51,24 @@
</div>
</div>
</div>
<div class="row">
<div class="col">
@if(($x=$o->getOtherTags())->count())
<div class="ms-4 mt-4 alert alert-danger p-2" style="max-width: 30em; font-size: 0.80em;">
This entry has [<strong>{!! $x->flatten()->join('</strong>, <strong>') !!}</strong>] tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA. You can though manage those tags with an LDIF import.
</div>
@elseif(($x=$o->getLangMultiTags())->count())
<div class="ms-4 mt-4 alert alert-danger p-2" style="max-width: 30em; font-size: 0.80em;">
This entry has multi-language tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA. You can though manage those lang tags with an LDIF import.
</div>
@elseif(($x=$o->getLangTags())->count())
<div class="ms-4 mt-4 alert alert-warning p-2" style="max-width: 30em; font-size: 0.80em;">
This entry has language tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA yet. You can though manage those lang tags with an LDIF import.
</div>
@endif
</div>
</div>
@endsection
@section('main-content')
@ -48,12 +79,9 @@
<div class="main-card mb-3 card">
<div class="card-body">
<div class="card-header-tabs">
<ul class="nav nav-tabs">
<ul class="nav nav-tabs mb-0">
<li class="nav-item"><a data-bs-toggle="tab" href="#attributes" class="nav-link active">@lang('Attributes')</a></li>
<li class="nav-item"><a data-bs-toggle="tab" href="#internal" class="nav-link">@lang('Internal')</a></li>
@env(['local'])
<li class="nav-item"><a data-bs-toggle="tab" href="#debug" class="nav-link">@lang('Debug')</a></li>
@endenv
</ul>
<div class="tab-content">
@ -63,10 +91,57 @@
@csrf
<input type="hidden" name="dn" value="">
<div class="card-header border-bottom-0">
<div class="btn-actions-pane-right">
<div role="group" class="btn-group-sm nav btn-group">
@foreach($langtags->prepend(Entry::TAG_NOTAG)->push('+') as $tag)
<a data-bs-toggle="tab" href="#tab-lang-{{ $tag ?: '_default' }}" class="btn btn-outline-light border-dark-subtle @if(! $loop->index) active @endif @if($loop->last)ndisabled @endif">
@switch($tag)
@case(Entry::TAG_NOTAG)
<i class="fas fa-fw fa-border-none" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="@lang('No Lang Tag')"></i>
@break
@foreach ($o->getVisibleAttributes() as $ao)
<x-attribute-type :edit="true" :o="$ao"/>
@endforeach
@case('+')
<!-- @todo To implement -->
<i class="fas fa-fw fa-plus text-dark" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="@lang('Add Lang Tag')"></i>
@break
@default
<span class="f16" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" title="{{ strtoupper($tag) }}"><i class="flag {{ $tag }}"></i></span>
@endswitch
</a>
@endforeach
</div>
</div>
</div>
<div class="card-body">
<div class="tab-content">
@foreach($langtags as $tag)
<div class="tab-pane @if(! $loop->index) active @endif" id="tab-lang-{{ $tag ?: '_default' }}" role="tabpanel">
@switch($tag)
@case(Entry::TAG_NOTAG)
@foreach ($o->getVisibleAttributes($tag) as $ao)
<x-attribute-type :edit="true" :o="$ao" :langtag="$tag"/>
@endforeach
@break
@case('+')
<div class="ms-auto mt-4 alert alert-warning p-2" style="max-width: 30em; font-size: 0.80em;">
It is not possible to create new language tags at the moment. This functionality should come soon.<br>
You can create them with an LDIF import though.
</div>
@break
@default
@foreach ($o->getVisibleAttributes($langtag=sprintf('lang-%s',$tag)) as $ao)
<x-attribute-type :edit="true" :o="$ao" :langtag="$langtag"/>
@endforeach
@endswitch
</div>
@endforeach
</div>
</div>
@include('fragment.dn.add_attr')
</form>
@ -80,26 +155,11 @@
</div>
<!-- Internal Attributes -->
<div class="tab-pane" id="internal" role="tabpanel">
<div class="tab-pane mt-3" id="internal" role="tabpanel">
@foreach ($o->getInternalAttributes() as $ao)
<x-attribute-type :o="$ao"/>
@endforeach
</div>
<!-- Debug -->
<div class="tab-pane" id="debug" role="tabpanel">
<div class="row">
<div class="col-4">
@dump($o)
</div>
<div class="col-4">
@dump($o->getAttributes())
</div>
<div class="col-4">
@dump(['available'=>$o->getAvailableAttributes()->pluck('name'),'missing'=>$o->getMissingAttributes()->pluck('name')])
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,7 +1,12 @@
@extends('home')
@section('page_title')
@include('fragment.dn.header')
@include('fragment.dn.header',[
'langtags'=>($langtags=$o->getLangTags()
->flatMap(fn($item)=>$item->values())
->unique()
->sort())
])
@endsection
@section('main-content')
@ -23,6 +28,7 @@
<thead>
<tr>
<th>Attribute</th>
<th>Tag</th>
<th>OLD</th>
<th>NEW</th>
</tr>
@ -31,17 +37,22 @@
<tbody>
@foreach ($o->getObjects()->filter(fn($item)=>$item->isDirty()) as $key => $oo)
<tr>
<th rowspan="{{ $x=max($oo->values->keys()->max(),$oo->values_old->keys()->max())+1}}">
<th rowspan="{{ $x=max($oo->values->dot()->keys()->count(),$oo->values_old->dot()->keys()->count())+1}}">
<abbr title="{{ $oo->description }}">{{ $oo->name }}</abbr>
</th>
@for($xx=0;$xx<$x;$xx++)
@if($xx)
@foreach($oo->values->dot()->keys()->merge($oo->values_old->dot()->keys())->unique() as $dotkey)
@if($loop->index)
</tr><tr>
@endif
<td>{{ (($r=$oo->render_item_old($xx)) !== NULL) ? $r : '['.strtoupper(__('New Value')).']' }}</td>
<td>{{ (($r=$oo->render_item_new($xx)) !== NULL) ? $r : '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[]" value="{{ Arr::get($oo->values,$xx) }}"></td>
@endfor
<th>
{{ $dotkey }}
</th>
<td>{{ (($r=$oo->render_item_old($dotkey)) !== NULL) ? $r : '['.strtoupper(__('New Value')).']' }}</td>
<td>{{ (($r=$oo->render_item_new($dotkey)) !== NULL) ? $r : '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[{{ collect(explode('.',$dotkey))->first() }}][]" value="{{ Arr::get($oo,$dotkey) }}"></td>
@endforeach
</tr>
@endforeach
</tbody>