Fix display of password attributes and update processing with jpegphoto and password

This commit is contained in:
Deon George 2024-01-09 17:44:50 +11:00
parent c8fffd6d81
commit c02f390f64
9 changed files with 237 additions and 66 deletions

View File

@ -10,7 +10,7 @@ use App\Classes\LDAP\Schema\AttributeType;
/** /**
* Represents an attribute of an LDAP Object * Represents an attribute of an LDAP Object
*/ */
class Attribute class Attribute implements \Countable, \ArrayAccess
{ {
// Attribute Name // Attribute Name
protected string $name; protected string $name;
@ -41,6 +41,9 @@ class Attribute
// RFC3866 Language Tags // RFC3866 Language Tags
protected Collection $lang_tags; protected Collection $lang_tags;
// The old values for this attribute - helps with isDirty() to determine if there is an update pending
protected Collection $oldValues;
/* /*
# Has the attribute been modified # Has the attribute been modified
protected $modified = false; protected $modified = false;
@ -98,6 +101,7 @@ class Attribute
$this->values = collect($values); $this->values = collect($values);
$this->lang_tags = collect(); $this->lang_tags = collect();
$this->required_by = collect(); $this->required_by = collect();
$this->oldValues = collect();
// No need to load our schema for internal attributes // No need to load our schema for internal attributes
if (! $this->is_internal) if (! $this->is_internal)
@ -151,6 +155,31 @@ class Attribute
return $this->__get('name'); return $this->__get('name');
} }
public function count(): int
{
return $this->values->count();
}
public function offsetExists(mixed $offset): bool
{
return ! is_null($this->values->get($offset));
}
public function offsetGet(mixed $offset): mixed
{
return $this->values->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
{
// We cannot set new values using array syntax
}
public function offsetUnset(mixed $offset): void
{
// We cannot clear values using array syntax
}
/** /**
* Return the hints about this attribute, ie: RDN, Required, etc * Return the hints about this attribute, ie: RDN, Required, etc
* *
@ -179,6 +208,24 @@ class Attribute
return $result->toArray(); return $result->toArray();
} }
/**
* Determine if this attribute has changes
*
* @return bool
*/
public function isDirty(): bool
{
if ($this->oldValues->count() !== $this->values->count())
return TRUE;
return $this->values->diff($this->oldValues)->count() !== 0;
}
public function oldValues(array $array): void
{
$this->oldValues = collect($array);
}
/** /**
* Display the attribute value * Display the attribute value
* *
@ -190,6 +237,7 @@ class Attribute
{ {
return view('components.attribute') return view('components.attribute')
->with('edit',$edit) ->with('edit',$edit)
->with('new',FALSE)
->with('o',$this); ->with('o',$this);
} }

View File

@ -5,12 +5,15 @@ namespace App\Classes\LDAP\Attribute\Binary;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Binary; use App\Classes\LDAP\Attribute\Binary;
use App\Traits\MD5Updates;
/** /**
* Represents an JpegPhoto Attribute * Represents an JpegPhoto Attribute
*/ */
final class JpegPhoto extends Binary final class JpegPhoto extends Binary
{ {
use MD5Updates;
public function __construct(string $name,array $values) public function __construct(string $name,array $values)
{ {
parent::__construct($name,$values); parent::__construct($name,$values);

View File

@ -5,12 +5,15 @@ namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/** /**
* Represents an attribute whose values are passwords * Represents an attribute whose values are passwords
*/ */
final class Password extends Attribute final class Password extends Attribute
{ {
use MD5Updates;
public function render(bool $edit=FALSE,bool $blank=FALSE): View public function render(bool $edit=FALSE,bool $blank=FALSE): View
{ {
return view('components.attribute.password') return view('components.attribute.password')

View File

@ -15,6 +15,100 @@ class Entry extends Model
/* OVERRIDES */ /* OVERRIDES */
public function getAttributes(): array public function getAttributes(): array
{
return $this->getAttributesAsObjects()->toArray();
}
/**
* Determine if the new and old values for a given key are equivalent.
*/
protected function originalIsEquivalent(string $key): bool
{
if (! array_key_exists($key, $this->original)) {
return false;
}
$current = $this->attributes[$key];
$original = $this->original[$key];
if ($current === $original) {
return true;
}
//dump(['key'=>$key,'current'=>$current,'original'=>$this->original[$key],'objectvalue'=>$this->getAttributeAsObject($key)->isDirty()]);
return ! $this->getAttributeAsObject($key)->isDirty();
}
public function getOriginal(): array
{
static $result = NULL;
if (is_null($result)) {
$result = collect();
// @todo Optimise this foreach with getAttributes()
foreach (parent::getOriginal() as $attribute => $value) {
// If the attribute name has language tags
$matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
// If the attribute doesnt exist we'll create it
$o = Arr::get($result,$attribute,Factory::create($attribute,[]));
$o->setLangTag($matches[3],$value);
} else {
$o = Factory::create($attribute,$value);
}
if (! $result->has($attribute)) {
// Set the rdn flag
if (preg_match('/^'.$attribute.'=/i',$this->dn))
$o->setRDN();
// Set required flag
$o->required_by(collect($this->getAttribute('objectclass')));
$result->put($attribute,$o);
}
}
}
return $result->toArray();
}
/* ATTRIBUTES */
/**
* Return a key to use for sorting
*
* @todo This should be the DN in reverse order
* @return string
*/
public function getSortKeyAttribute(): string
{
return $this->getDn();
}
/* METHODS */
/**
* Get an attribute as an object
*
* @param string $key
* @return Attribute|null
*/
public function getAttributeAsObject(string $key): Attribute|null
{
return Arr::get($this->getAttributesAsObjects(),$key);
}
/**
* Convert all our attribute values into an array of Objects
*
* @return Collection
*/
protected function getAttributesAsObjects(): Collection
{ {
static $result = NULL; static $result = NULL;
@ -43,6 +137,9 @@ class Entry extends Model
// Set required flag // Set required flag
$o->required_by(collect($this->getAttribute('objectclass'))); $o->required_by(collect($this->getAttribute('objectclass')));
// Store our original value to know if this attribute has changed
$o->oldValues(Arr::get($this->original,$attribute));
$result->put($attribute,$o); $result->put($attribute,$o);
} }
} }
@ -73,36 +170,12 @@ class Entry extends Model
// Case where at least one attribute or its friendly name is in $attrs_display_order // Case where at least one attribute or its friendly name is in $attrs_display_order
// return -1 if $a before $b in $attrs_display_order // return -1 if $a before $b in $attrs_display_order
return ($a_key < $b_key) ? -1 : 1; return ($a_key < $b_key) ? -1 : 1;
} ])->toArray(); } ]);
} }
return $result; return $result;
} }
/* ATTRIBUTES */
/**
* Return a key to use for sorting
*
* @todo This should be the DN in reverse order
* @return string
*/
public function getSortKeyAttribute(): string
{
return $this->getDn();
}
/* METHODS */
/**
* Return a secure version of the DN
* @return string
*/
public function getDNSecure(): string
{
return Crypt::encryptString($this->getDn());
}
/** /**
* Return a list of available attributes - as per the objectClass entry of the record * Return a list of available attributes - as per the objectClass entry of the record
* *
@ -118,6 +191,15 @@ class Entry extends Model
return $result; return $result;
} }
/**
* Return a secure version of the DN
* @return string
*/
public function getDNSecure(): string
{
return Crypt::encryptString($this->getDn());
}
/** /**
* Return a list of LDAP internal attributes * Return a list of LDAP internal attributes
* *

23
app/Traits/MD5Updates.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* Determine if a value has changed by comparing its MD5 value
*/
namespace App\Traits;
use Illuminate\Support\Arr;
trait MD5Updates
{
public function isDirty(): bool
{
if (! parent::isDirty())
return TRUE;
foreach ($this->values->diff($this->oldValues) as $key => $value)
if (md5(Arr::get($this->oldValues,$key)) !== $value)
return TRUE;
return FALSE;
}
}

View File

@ -1,33 +1,40 @@
<!-- $o=Binary\JpegPhoto::class --> <!-- $o=Binary\JpegPhoto::class -->
@if($edit) <!-- @todo We are not handling redirect backs with updated photos -->
<div class="input-group has-validation @if($e=$errors->get($o->name_lc))is-invalid @endif"> <div class="row pt-2">
@endif <div class="col-1"></div>
<div class="col-10 p-2">
<div id="{{ $o->name_lc }}">
<table class="table table-borderless p-0 m-0"> <table class="table table-borderless p-0 m-0">
<tr>
@foreach ($o->values as $value) @foreach ($o->values as $value)
<div class="input-group has-validation">
<tr>
@switch ($x=$f->buffer($value,FILEINFO_MIME_TYPE)) @switch ($x=$f->buffer($value,FILEINFO_MIME_TYPE))
@case('image/jpeg') @case('image/jpeg')
@default @default
<td> <td>
<input type="hidden" name="{{ $o->name_lc }}[]" value="{{ md5($value) }}"> <input type="hidden" name="{{ $o->name_lc }}[]" value="{{ md5($value) }}">
<img class="jpegphoto" src="data:{{ $x }};base64, {{ base64_encode($value) }}" /> <img class="jpegphoto" src="data:{{ $x }};base64, {{ base64_encode($value) }}" @if($e=$errors->get($o->name_lc.'.'.$loop->index))is-invalid @endif />
@if ($edit) @if ($edit)
<br><span class="btn btn-sm btn-danger deletable d-none"><i class="fas fa-trash-alt"></i> @lang('Delete')</span> <br>
@endif <!-- @todo TO IMPLEMENT -->
</td> <span class="btn btn-sm btn-danger deletable d-none"><i class="fas fa-trash-alt"></i> @lang('Delete')</span>
@endswitch
@endforeach
</tr>
</table>
@if($edit)
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)
{{ join('|',$e) }} {{ join('|',$e) }}
@endif @endif
</div> </div>
</div>
@endif @endif
</td>
@endswitch
</tr>
</div>
@endforeach
</table>
</div>
<!-- @todo TO IMPLEMENT -->
@include('components.attribute.widget.options')
</div>
</div>

View File

@ -1,11 +1,14 @@
<!-- $o=Password::class --> <!-- $o=Password::class -->
<div class="row"> <!-- @todo We are not handling redirect backs with updated values -->
<div class="col-12"> <div class="row pt-2">
<div class="col-1"></div>
<div class="col-10 p-2">
<div id="{{ $o->name_lc }}"> <div id="{{ $o->name_lc }}">
@foreach (old($o->name_lc,$o->values) as $value) @foreach ($o->values as $value)
@if ($edit) @if ($edit)
<div class="input-group has-validation"> <div class="input-group has-validation">
<input type="password" class="form-control @if($e=$errors->get($o->name_lc.'.'.$loop->index))is-invalid @endif mb-1 @if($o->values->search($value) === FALSE) border-focus @endif" name="{{ $o->name_lc }}[]" value="{{ str_repeat('*',10) }}" readonly="true"> <input type="password" class="form-control @if($e=$errors->get($o->name_lc.'.'.$loop->index))is-invalid @endif mb-1 @if($o->values->search($value) === FALSE) border-focus @endif" name="{{ $o->name_lc }}[]" value="{{ md5($value) }}" readonly="true">
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)
{{ join('|',$e) }} {{ join('|',$e) }}
@ -17,9 +20,11 @@
@endif @endif
@endforeach @endforeach
</div> </div>
</div>
<div class="col-12 col-sm-6 col-lg-4"> @include('components.attribute.widget.options')
<span class="btn btn-sm btn-outline-dark mt-3 mb-3"><i class="fas fa-user-check"></i> @lang('Check Password')</span>
<span class="p-0 m-0">
<span class="btn btn-sm btn-outline-dark mt-3"><i class="fas fa-user-check"></i> @lang('Check Password')</span>
</span>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
@if($o->is_rdn) @if($o->is_rdn)
<span class="btn btn-sm btn-outline-focus mt-3"><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span> <span class="btn btn-sm btn-outline-focus mt-3"><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
@elseif($edit && $o->can_addvalues) @elseif($edit && $o->can_addvalues)
<div class="p-0 m-0"> <span class="p-0 m-0">
<span class="btn btn-sm btn-outline-primary mt-3 addable @if(! $new)d-none @endif" id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span> <span class="btn btn-sm btn-outline-primary mt-3 addable @if(! $new)d-none @endif" id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
@if($new) @if($new)
<script type="text/javascript"> <script type="text/javascript">
@ -15,7 +15,7 @@
}); });
</script> </script>
@endif @endif
</div> </span>
@endif @endif
@section('page-scripts') @section('page-scripts')

View File

@ -3,7 +3,7 @@
@section('page_title') @section('page_title')
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
<td class="{{ ($x=Arr::get($o->getAttributes(),'jpegphoto')) ? 'border' : '' }}" rowspan="2">{!! $x ? $x->render() : sprintf('<div class="page-title-icon f32"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!}</td> <td class="{{ ($x=Arr::get($o->getOriginal(),'jpegphoto')) ? 'border' : '' }}" rowspan="2">{!! $x ? $x->render() : sprintf('<div class="page-title-icon f32"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!}</td>
<td class="text-end align-text-top p-0 {{ $x ? 'ps-5' : 'pt-2' }}"><strong>{{ $dn }}</strong></td> <td class="text-end align-text-top p-0 {{ $x ? 'ps-5' : 'pt-2' }}"><strong>{{ $dn }}</strong></td>
</tr> </tr>
<tr> <tr>
@ -89,7 +89,7 @@
</tr><tr> </tr><tr>
@endif @endif
<td>{{ Arr::get(Arr::get($o->getOriginal(),$key,['['.strtoupper(__('New Value')).']']),$xx) }}</td> <td>{{ Arr::get(Arr::get($o->getOriginal(),$key),$xx,'['.strtoupper(__('New Value')).']') }}</td>
<td>{{ $y=Arr::get($value,$xx) }}<input type="hidden" name="{{ $key }}[]" value="{{ $y }}"></td> <td>{{ $y=Arr::get($value,$xx) }}<input type="hidden" name="{{ $key }}[]" value="{{ $y }}"></td>
@endfor @endfor
</tr> </tr>