Compare commits

..

8 Commits

Author SHA1 Message Date
817b72cdac Add some attribute tags messages when we cant handle some attributes.
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m23s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-03-19 09:39:15 +11:00
6eb20a071c Import and Export work with attribute tags 2025-03-19 09:39:15 +11:00
47662ba091 Remove deprecated Attribute::lang_tags 2025-03-19 09:39:14 +11:00
f83beece87 Fix export to work with no_attr_tags 2025-03-19 09:39:14 +11:00
0a074d6121 Handle no attribute tags at an Attribute::class level, added form/disabled components 2025-03-19 09:39:14 +11:00
05ab9ed346 Internal attributes are now handled by the new backend setup for attribute tags 2025-03-19 09:39:14 +11:00
3fc6b55757 Start of work to handle attribute tags - should help with #75 and #16 2025-03-19 09:39:14 +11:00
80a329afc2 Revert version to 2.1.0-dev 2025-03-19 09:39:14 +11:00
32 changed files with 270 additions and 396 deletions

View File

@ -61,7 +61,6 @@ 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,12 +7,11 @@ 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
class Attribute implements \Countable, \ArrayAccess, \Iterator
{
// Attribute Name
protected string $name;
@ -101,9 +100,9 @@ class Attribute implements \Countable, \ArrayAccess
{
$this->dn = $dn;
$this->name = $name;
$this->_values = collect($values);
$this->_values_old = collect($values);
$this->values_old = collect($values);
$this->values = collect();
$this->oc = collect($oc);
$this->schema = (new Server)
@ -150,15 +149,15 @@ class Attribute implements \Countable, \ArrayAccess
// 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,
'values' => $this->no_attr_tags ? collect($this->_values->first()) : $this->_values,
// The original attribute values
'values_old' => $this->no_attr_tags ? $this->tagValuesOld() : $this->_values_old,
'values_old' => $this->no_attr_tags ? collect($this->_values_old->first()) : $this->_values_old,
default => throw new \Exception('Unknown key:' . $key),
};
}
public function __set(string $key,mixed $values): void
public function __set(string $key,mixed $values)
{
switch ($key) {
case 'values':
@ -179,21 +178,53 @@ class Attribute implements \Countable, \ArrayAccess
return $this->name;
}
/* INTERFACE */
public function addValue(string $tag,string $value): void
{
$this->_values->put(
$tag,
$this->_values
->get($tag,collect())
->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;
}
public function count(): int
{
return $this->_values->dot()->count();
return $this->values->count();
}
public function offsetExists(mixed $offset): bool
{
return $this->_values->dot()->has($offset);
return ! is_null($this->values->has($offset));
}
public function offsetGet(mixed $offset): mixed
{
return $this->_values->dot()->get($offset);
return $this->values->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
@ -206,24 +237,6 @@ class Attribute implements \Countable, \ArrayAccess
// 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
*
@ -253,10 +266,8 @@ class Attribute implements \Countable, \ArrayAccess
*/
public function isDirty(): bool
{
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);
return ($this->values_old->count() !== $this->values->count())
|| ($this->values->diff($this->values_old)->count() !== 0);
}
/**
@ -296,14 +307,14 @@ class Attribute implements \Countable, \ArrayAccess
->with('new',$new);
}
public function render_item_old(string $dotkey): ?string
public function render_item_old(int $key): ?string
{
return Arr::get($this->values_old->dot(),$dotkey);
return Arr::get($this->values_old,$key);
}
public function render_item_new(string $dotkey): ?string
public function render_item_new(int $key): ?string
{
return Arr::get($this->values->dot(),$dotkey);
return Arr::get($this->values,$key);
}
/**
@ -315,21 +326,7 @@ class Attribute implements \Countable, \ArrayAccess
{
// If we dont have any objectclasses then we cant know if it is required
return $this->oc->count()
? $this->oc->intersect($this->required_by->keys())->sort()
? $this->oc->intersect($this->schema->required_by_object_classes->keys())->sort()
: collect();
}
public function tagValues(string $tag=Entry::TAG_NOTAG): Collection
{
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,7 +5,6 @@ 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;
/**
@ -15,14 +14,13 @@ final class JpegPhoto extends Binary
{
use MD5Updates;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): 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

@ -26,16 +26,18 @@ final class KrbPrincipalKey extends Attribute
->with('new',$new);
}
public function render_item_old(string $dotkey): ?string
public function render_item_old(int $key): ?string
{
return parent::render_item_old($dotkey)
$pw = Arr::get($this->values_old,$key);
return $pw
? str_repeat('*',16)
: NULL;
}
public function render_item_new(string $dotkey): ?string
public function render_item_new(int $key): ?string
{
return parent::render_item_new($dotkey)
$pw = Arr::get($this->values,$key);
return $pw
? 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

View File

@ -29,33 +29,19 @@ final class ObjectClass extends Attribute
{
parent::__construct($dn,$name,$values,['top']);
$this->set_oc_schema($this->tagValuesOld());
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$this->values_old->contains($item->name));
}
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
*
@ -77,11 +63,4 @@ 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

@ -88,23 +88,19 @@ final class Password extends Attribute
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort());
}
public function render_item_old(string $dotkey): ?string
public function render_item_old(int $key): ?string
{
$pw = parent::render_item_old($dotkey);
$pw = Arr::get($this->values_old,$key);
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(string $dotkey): ?string
public function render_item_new(int $key): ?string
{
$pw = parent::render_item_new($dotkey);
$pw = Arr::get($this->values,$key);
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

@ -5,7 +5,6 @@ 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
@ -56,8 +55,8 @@ class LDIF extends Export
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))
? sprintf('%s: %s',$ao->name.($tag ? ';'.$tag : ''),$value)
: sprintf('%s:: %s',$ao->name.($tag ? ';'.$tag : ''),base64_encode($value))
,$this->br);
}
}

View File

@ -209,8 +209,7 @@ final class Server
/**
* Obtain the rootDSE for the server, that gives us server information
*
* @param string|null $connection
* @param Carbon|null $cachetime
* @param null $connection
* @return Entry|null
* @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE();
@ -231,7 +230,7 @@ final class Server
/**
* Get the Schema DN
*
* @param string|null $connection
* @param $connection
* @return string
* @throws ObjectNotFoundException
*/
@ -246,21 +245,16 @@ 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,array $attrs=['dn']): ?LDAPCollection
public function children(string $dn): ?LDAPCollection
{
return ($x=(new Entry)
->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->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
]))
->select(['*','hassubordinates'])
->setDn($dn)
->list()
->orderBy('dn')
->get()) ? $x : NULL;
}
@ -534,6 +528,7 @@ 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 = [Entry::TAG_NOTAG=>$x];
$o->objectclass = $x;
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
$o->{$ao->name} = '';
$o->setRDNBase($key['dn']);
}
@ -188,6 +188,8 @@ class HomeController extends Controller
$result = (new Entry)
->query()
//->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
//->select(['*'])
->setDn($dn)
->recursive()
->get();
@ -267,22 +269,19 @@ class HomeController extends Controller
// We need to process and encrypt the password
if ($request->userpassword) {
$passwords = [];
$po = $o->getObject('userpassword');
foreach (Arr::dot($request->userpassword) as $dotkey => $value) {
foreach ($request->userpassword as $key => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
$passwords[$dotkey] = $value;
if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) {
array_push($passwords,$old);
continue;
}
if ($value) {
$type = Arr::get($request->userpassword_hash,$dotkey);
$passwords[$dotkey] = Password::hash_id($type)
->encode($value);
$type = Arr::get($request->userpassword_hash,$key);
array_push($passwords,Password::hash_id($type)->encode($value));
}
}
$o->userpassword = Arr::undot($passwords);
$o->userpassword = $passwords;
}
if (! $o->getDirty())
@ -387,13 +386,7 @@ class HomeController extends Controller
'dn' => $view
->with('dn',$key['dn'])
->with('o',$o)
->with('page_actions',collect([
'copy'=>FALSE,
'create'=>FALSE,
'delete'=>TRUE,
'edit'=>TRUE,
'export'=>TRUE,
])),
->with('page_actions',collect(['edit'=>TRUE])),
'import' => $view,

View File

@ -14,16 +14,10 @@ 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;
@ -54,9 +48,8 @@ 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
{
@ -65,7 +58,7 @@ class Entry extends Model
($item->no_attr_tags)
? [strtolower($item->name)=>$item->values]
: $item->values
->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v]))
->flatMap(fn($v,$k)=>[strtolower($item->name.($k ? ';'.$k : ''))=>$v]))
->toArray();
}
@ -78,10 +71,12 @@ class Entry extends Model
{
$key = $this->normalizeAttributeKey($key);
list($attribute,$tag) = $this->keytag($key);
// @todo Silently ignore keys of language tags - we should work with them
if (str_contains($key,';'))
return TRUE;
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($attribute)))
|| (! $this->getObject($attribute)->isDirty());
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($key)))
|| (! $this->getObject($key)->isDirty());
}
public static function query(bool $noattrs=false): Builder
@ -98,22 +93,18 @@ 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
{
foreach ($value as $k => $v)
parent::setAttribute($key.($k !== self::TAG_NOTAG ? ';'.$k : ''),$v);
parent::setAttribute($key,$value);
$key = $this->normalizeAttributeKey($key);
list($attribute,$tags) = $this->keytag($key);
$o = $this->objects->get($attribute) ?: Factory::create($this->dn ?: '',$attribute,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($value);
$o = $this->objects->get($key) ?: Factory::create($this->dn ?: '',$key,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($this->attributes[$key]);
$this->objects->put($key,$o);
@ -177,13 +168,21 @@ class Entry extends Model
$key = $this->normalizeAttributeKey(strtolower($key));
// If the attribute name has tags
list($attribute,$tag) = $this->keytag($key);
$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 = '';
}
if (! config('server')->schema('attributetypes')->has($attribute))
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
$o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]);
$o->addValue($tag,[$value]);
$o->addValue($tags,$value);
$this->objects->put($attribute,$o);
}
@ -199,7 +198,16 @@ class Entry extends Model
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
foreach ($this->attributes as $attrtag => $values) {
list($attribute,$tags) = $this->keytag($attrtag);
// If the attribute name has tags
$matches = [];
if (preg_match(sprintf('/^([%s]+);+([%s;]+)/',self::TAG_CHARS,self::TAG_CHARS),$attrtag,$matches)) {
$attribute = $matches[1];
$tags = $matches[2];
} else {
$attribute = $attrtag;
$tags = NULL;
}
$orig = Arr::get($this->original,$attrtag,[]);
@ -214,8 +222,7 @@ class Entry extends Model
$entry_oc,
));
$o->addValue($tags,$values);
$o->addValueOld($tags,Arr::get($this->original,$attrtag));
$o->values = $o->values->merge([$tags=>$values]);
$result->put($attribute,$o);
}
@ -258,7 +265,7 @@ class Entry extends Model
{
$result = collect();
foreach ($this->getObject('objectclass')->values as $oc)
foreach ($this->objectclass as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes);
return $result;
@ -297,9 +304,7 @@ class Entry extends Model
->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)=>preg_match(sprintf('/%s+;?/',self::TAG_CHARS_LANG),$item)))
->filter(fn($item)=>$item->count());
}
@ -357,8 +362,7 @@ class Entry extends Model
->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(sprintf('/^%s+$/',self::TAG_CHARS_LANG),$item))
&& (! preg_match('/^binary$/',$item))
)
->count())
@ -370,9 +374,6 @@ class Entry extends Model
* 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
{
@ -393,14 +394,12 @@ class Entry extends Model
/**
* Return this list of user attributes
*
* @param string|null $tag If null return all tags
* @return Collection
*/
public function getVisibleAttributes(?string $tag=NULL): Collection
public function getVisibleAttributes(): Collection
{
return $this->objects
->filter(fn($item)=>! $item->is_internal)
->filter(fn($item)=>is_null($tag) || count($item->tagValues($tag)) > 0);
->filter(fn($item)=>! $item->is_internal);
}
public function hasAttribute(int|string $key): bool
@ -446,92 +445,65 @@ class Entry extends Model
*/
public function icon(): string
{
$objectclasses = $this->getObject('objectclass')
->tagValues()
->map(fn($item)=>strtolower($item));
$objectclasses = array_map('strtolower',$this->objectclass);
// Return icon based upon objectClass value
if ($objectclasses->intersect([
'account',
'inetorgperson',
'organizationalperson',
'person',
'posixaccount',
])->count())
if (in_array('person',$objectclasses) ||
in_array('organizationalperson',$objectclasses) ||
in_array('inetorgperson',$objectclasses) ||
in_array('account',$objectclasses) ||
in_array('posixaccount',$objectclasses))
return 'fas fa-user';
elseif ($objectclasses->contains('organization'))
elseif (in_array('organization',$objectclasses))
return 'fas fa-university';
elseif ($objectclasses->contains('organizationalunit'))
elseif (in_array('organizationalunit',$objectclasses))
return 'fas fa-object-group';
elseif ($objectclasses->intersect([
'posixgroup',
'groupofnames',
'groupofuniquenames',
'group',
])->count())
elseif (in_array('posixgroup',$objectclasses) ||
in_array('groupofnames',$objectclasses) ||
in_array('groupofuniquenames',$objectclasses) ||
in_array('group',$objectclasses))
return 'fas fa-users';
elseif ($objectclasses->intersect([
'dcobject',
'domainrelatedobject',
'domain',
'builtindomain',
])->count())
elseif (in_array('dcobject',$objectclasses) ||
in_array('domainrelatedobject',$objectclasses) ||
in_array('domain',$objectclasses) ||
in_array('builtindomain',$objectclasses))
return 'fas fa-network-wired';
elseif ($objectclasses->contains('alias'))
elseif (in_array('alias',$objectclasses))
return 'fas fa-theater-masks';
elseif ($objectclasses->contains('country'))
return sprintf('flag %s',strtolower(Arr::get($this->c ?: [],0)));
elseif (in_array('country',$objectclasses))
return sprintf('flag %s',strtolower(Arr::get($this->c,0)));
elseif ($objectclasses->contains('device'))
elseif (in_array('device',$objectclasses))
return 'fas fa-mobile-alt';
elseif ($objectclasses->contains('document'))
elseif (in_array('document',$objectclasses))
return 'fas fa-file-alt';
elseif ($objectclasses->contains('iphost'))
elseif (in_array('iphost',$objectclasses))
return 'fas fa-wifi';
elseif ($objectclasses->contains('room'))
elseif (in_array('room',$objectclasses))
return 'fas fa-door-open';
elseif ($objectclasses->contains('server'))
elseif (in_array('server',$objectclasses))
return 'fas fa-server';
elseif ($objectclasses->contains('openldaprootdse'))
elseif (in_array('openldaprootdse',$objectclasses))
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_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))
foreach ($this->values->diff($this->values_old) as $key => $value)
if (md5(Arr::get($this->values_old,$key)) !== $value)
return TRUE;
return FALSE;

View File

@ -5,7 +5,6 @@ namespace App\View\Components;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Ldap\Entry;
class Attribute extends Component
{
@ -13,32 +12,30 @@ class Attribute extends Component
public bool $edit;
public bool $new;
public bool $old;
public string $langtag;
public ?string $na; // Text to render if the LDAPAttribute is null
public ?string $na;
/**
* 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)
{
/**
* Create a new component instance.
*/
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?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,25 +4,23 @@ 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,string $langtag=Entry::TAG_NOTAG)
public function __construct(LDAPAttribute $o,bool $new=FALSE)
{
$this->o = $o;
$this->new = $new;
$this->langtag = $langtag;
}
/**
@ -32,7 +30,6 @@ class AttributeType extends Component
{
return view('components.attribute-type')
->with('o',$this->o)
->with('new',$this->new)
->with('langtag',$this->langtag);
->with('new',$this->new);
}
}

View File

@ -30,10 +30,7 @@ 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

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

View File

@ -1,21 +1,31 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
<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))>
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $tag=>$tagvalues)
<div class="row p-2 border rounded">
<div class="col-2">
{{ $tag }}
</div>
<div class="col-10">
<div class="row">
<div class="col-12">
@foreach($tagvalues 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="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
@else
{{ $value }}
@endif
@endforeach
</div>
</div>
@else
<input type="text" class="form-control mb-1" value="{{ $value }}" disabled>
@endif
@endforeach
</div>
</div>
</div>
@endforeach
</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 ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
<x-attribute.layout :edit="$edit" :new="false" :o="$o">
<table class="table table-borderless p-0 m-0">
@foreach($o->tagValuesOld() as $key => $value)
@foreach ($o->values_old as $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 }}[{{ $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) }}" />
<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) }}" />
@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,10 +1,10 @@
<!-- @todo We are not handling redirect backs yet with updated passwords -->
<!-- $o=KrbPrincipleKey::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o" :langtag="$langtag">
@foreach($o->tagValuesOld($langtag) as $key => $value)
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach($o->values_old as $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->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" 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->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ md5($value) }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
@ -13,7 +13,7 @@
</div>
</div>
@else
{{ $o->render_item_old($langtag.'.'.$key) }}
{{ str_repeat('*',16) }}
@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(Arr::get(old($o->name_lc,[$langtag=>$o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@foreach(($o->values->count() ? $o->values : ($new ? [0] : NULL)) as $value)
@if($edit)
<div id="32"></div>
<div id="16"></div>
<div class="input-group has-validation mb-3">
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" @readonly(true)>
<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)>
<div class="invalid-feedback pb-2">
@if($e=$errors->get($o->name_lc.'.'.$loop->index))
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
@else
{{ $o->render_item_old($langtag.'.'.$key) }}
{{ $value }}
@endif
@endforeach
</x-attribute.layout>

View File

@ -1,12 +1,12 @@
<!-- $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)
<!-- $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" :langtag="$langtag"/>
<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value"/>
@else
{{ $o->render_item_old($langtag.'.'.$key) }}
{{ $value }}
@if ($o->isStructural($value))
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}">
<input type="hidden" name="{{ $o->name_lc }}[]" 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" :langtag="$langtag">
@foreach($o->tagValuesOld($langtag) as $key => $value)
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach($o->values_old as $value)
@if($edit)
<div class="input-group has-validation mb-3">
<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)>
<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)>
<div class="invalid-feedback pb-2">
@if($e)
@ -14,7 +14,7 @@
</div>
</div>
@else
{{ $o->render_item_old($langtag.'.'.$key) }}
{{ (($x=$o->hash($value)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '' }}{{ str_repeat('*',16) }}
@endif
@endforeach
</x-attribute.layout>

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->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" 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->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)>
@if ($o->isStructural($value))
<span class="input-group-end text-black-50">@lang('structural')</span>
<span class="input-group-end text-black-50">structural</span>
@else
<span class="input-group-end"><i class="fas fa-fw fa-xmark"></i></span>
@endif

View File

@ -3,7 +3,8 @@
@php($clone=FALSE)
<span class="p-0 m-0">
@if($o->is_rdn)
<button class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</button>
<br/>
<span class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
@elseif($edit && $o->can_addvalues)
@switch(get_class($o))
@case(JpegPhoto::class)
@ -228,11 +229,8 @@
// 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')]')
.addClass('border-focus')
.appendTo('#'+item.currentTarget.id.replace('-addnew',''));
cln.find('input:last').attr('value','').attr('placeholder', '[@lang('NEW')]');
cln.appendTo('#'+item.currentTarget.id.replace('-addnew',''));
});
});
</script>

View File

@ -1,8 +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)
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) 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>
<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>

View File

@ -1,8 +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)
@foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $value)
<div class="input-group">
<input type="text" class="form-control mb-1" value="{{ $value }}" disabled>
<input type="text" @class(['form-control','mb-1']) name="{{ $o->name_lc }}[]" value="{{ $value }}" @disabled(true)>
</div>
@endforeach
</x-attribute.layout>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,7 @@
@use(App\Ldap\Entry)
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header',[
'o'=>($o ?? $o=$server->fetch($dn)),
'langtags'=>($langtags=$o->getLangTags()
->flatMap(fn($item)=>$item->values())
->unique()
->sort())
])
@include('fragment.dn.header',['o'=>($o ?? $o=$server->fetch($dn))])
@endsection
@section('page_actions')
@ -17,30 +9,25 @@
<div class="col">
<div class="action-buttons float-end">
<ul class="nav">
@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'))
@if(isset($page_actions) && $page_actions->contains('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->get('copy'))
@if(isset($page_actions) && $page_actions->contains('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->get('edit'))
@if((isset($page_actions) && $page_actions->contains('edit')) || old())
<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->get('delete'))
@if(isset($page_actions) && $page_actions->contains('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>
@ -79,9 +66,12 @@
<div class="main-card mb-3 card">
<div class="card-body">
<div class="card-header-tabs">
<ul class="nav nav-tabs mb-0">
<ul class="nav nav-tabs">
<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">
@ -91,57 +81,10 @@
@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
@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>
@foreach ($o->getVisibleAttributes() as $ao)
<x-attribute-type :edit="true" :o="$ao"/>
@endforeach
@include('fragment.dn.add_attr')
</form>
@ -155,11 +98,26 @@
</div>
<!-- Internal Attributes -->
<div class="tab-pane mt-3" id="internal" role="tabpanel">
<div class="tab-pane" 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,12 +1,7 @@
@extends('home')
@section('page_title')
@include('fragment.dn.header',[
'langtags'=>($langtags=$o->getLangTags()
->flatMap(fn($item)=>$item->values())
->unique()
->sort())
])
@include('fragment.dn.header')
@endsection
@section('main-content')
@ -28,7 +23,6 @@
<thead>
<tr>
<th>Attribute</th>
<th>Tag</th>
<th>OLD</th>
<th>NEW</th>
</tr>
@ -37,22 +31,17 @@
<tbody>
@foreach ($o->getObjects()->filter(fn($item)=>$item->isDirty()) as $key => $oo)
<tr>
<th rowspan="{{ $x=max($oo->values->dot()->keys()->count(),$oo->values_old->dot()->keys()->count())+1}}">
<th rowspan="{{ $x=max($oo->values->keys()->max(),$oo->values_old->keys()->max())+1}}">
<abbr title="{{ $oo->description }}">{{ $oo->name }}</abbr>
</th>
@foreach($oo->values->dot()->keys()->merge($oo->values_old->dot()->keys())->unique() as $dotkey)
@if($loop->index)
@for($xx=0;$xx<$x;$xx++)
@if($xx)
</tr><tr>
@endif
<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
<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
</tr>
@endforeach
</tbody>