Compare commits

...

10 Commits

Author SHA1 Message Date
d3d7881e3b Added additional password hashing functions
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 33s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m26s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s
2025-01-18 16:43:49 +11:00
77a139016b Fix when cloning an attribute, ensure we blank out the previous value. When processing request submission '0' could be a valid value. 2025-01-18 16:43:49 +11:00
5a922fe202 For rebuild of cache assets, since hashFiles() doesnt work 2025-01-18 16:43:49 +11:00
08e838d40a Foundation for Check Password and password functions - only Clear is currently implemented 2025-01-18 16:43:49 +11:00
30f964b849 Use our Attribute::class when rendering update_confirm 2025-01-18 16:43:49 +11:00
293f1ab9ce Remove usage of search() === to contains() 2025-01-18 16:43:49 +11:00
960e0de5c8 Fix to getDirty() when using MD5Updates Trait on attributes 2025-01-18 16:43:49 +11:00
6e06caa83b Some code optimisation and de-duplication with components 2025-01-18 16:43:48 +11:00
8b922b2e8b Add select2 bootstrap 5 theme 2025-01-18 16:43:48 +11:00
026b3f5a20 Use components for form buttons and file notes 2025-01-18 16:43:48 +11:00
62 changed files with 989 additions and 701 deletions

View File

@ -66,7 +66,7 @@ jobs:
public/js/manifest.js public/js/manifest.js
public/js/vendor.js public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }} #key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets key: build-pla-page-assets-5166780
#restore-keys: | #restore-keys: |
# build-pla-page-assets- # build-pla-page-assets-
@ -130,7 +130,7 @@ jobs:
public/js/manifest.js public/js/manifest.js
public/js/vendor.js public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }} #key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets key: build-pla-page-assets-5166780
#restore-keys: | #restore-keys: |
# build-pla-page-assets- # build-pla-page-assets-
@ -138,7 +138,7 @@ jobs:
name: List the state of page assets name: List the state of page assets
continue-on-error: false continue-on-error: false
run: | run: |
echo CACHE-HIT:${{ steps.cache-page-assets.outputs.cache-hit }} echo CACHE-MISS:${{ steps.cache-page-assets.outputs.cache-hit }}
ls -al public/css/ ls -al public/css/
ls -al public/js/ ls -al public/js/

View File

@ -3,6 +3,7 @@
namespace App\Classes\LDAP; namespace App\Classes\LDAP;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType; use App\Classes\LDAP\Schema\AttributeType;
@ -10,10 +11,11 @@ use App\Classes\LDAP\Schema\AttributeType;
/** /**
* Represents an attribute of an LDAP Object * Represents an attribute of an LDAP Object
*/ */
class Attribute implements \Countable, \ArrayAccess class Attribute implements \Countable, \ArrayAccess, \Iterator
{ {
// Attribute Name // Attribute Name
protected string $name; protected string $name;
private int $counter = 0;
protected ?AttributeType $schema = NULL; protected ?AttributeType $schema = NULL;
@ -173,6 +175,31 @@ class Attribute implements \Countable, \ArrayAccess
$this->values->push($value); $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;
}
public function count(): int public function count(): int
{ {
return $this->values->count(); return $this->values->count();
@ -180,7 +207,7 @@ class Attribute implements \Countable, \ArrayAccess
public function offsetExists(mixed $offset): bool public function offsetExists(mixed $offset): bool
{ {
return ! is_null($this->values->get($offset)); return ! is_null($this->values->has($offset));
} }
public function offsetGet(mixed $offset): mixed public function offsetGet(mixed $offset): mixed
@ -233,10 +260,8 @@ class Attribute implements \Countable, \ArrayAccess
*/ */
public function isDirty(): bool public function isDirty(): bool
{ {
if ($this->oldValues->count() !== $this->values->count()) return ($this->oldValues->count() !== $this->values->count())
return TRUE; || ($this->values->diff($this->oldValues)->count() !== 0);
return $this->values->diff($this->oldValues)->count() !== 0;
} }
public function oldValues(array $array): void public function oldValues(array $array): void
@ -247,9 +272,9 @@ class Attribute implements \Countable, \ArrayAccess
/** /**
* Display the attribute value * Display the attribute value
* *
* @param bool $edit * @param bool $edit Render an edit form
* @param bool $old * @param bool $old Use old value
* @param bool $new * @param bool $new Enable adding values
* @return View * @return View
*/ */
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): View
@ -261,6 +286,16 @@ class Attribute implements \Countable, \ArrayAccess
->with('new',$new); ->with('new',$new);
} }
public function render_item_old(int $key): ?string
{
return Arr::get($this->old_values,$key);
}
public function render_item_new(int $key): ?string
{
return Arr::get($this->values,$key);
}
/** /**
* Set the objectclasses that require this attribute * Set the objectclasses that require this attribute
* *

View File

@ -14,13 +14,6 @@ final class JpegPhoto extends Binary
{ {
use MD5Updates; use MD5Updates;
public function __construct(string $name,array $values)
{
parent::__construct($name,$values);
$this->internal = FALSE;
}
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): View
{ {
return view('components.attribute.binary.jpegphoto') return view('components.attribute.binary.jpegphoto')

View File

@ -36,7 +36,7 @@ final class ObjectClass extends Attribute
*/ */
public function isStructural(string $value): bool public function isStructural(string $value): bool
{ {
return $this->structural->search($value) !== FALSE; return $this->structural->contains($value);
} }
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): View

View File

@ -3,6 +3,8 @@
namespace App\Classes\LDAP\Attribute; namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;
@ -13,6 +15,65 @@ use App\Traits\MD5Updates;
final class Password extends Attribute final class Password extends Attribute
{ {
use MD5Updates; use MD5Updates;
private const password_helpers = 'Classes/LDAP/Attribute/Password';
public const commands = 'App\\Classes\\LDAP\\Attribute\\Password\\';
private static function helpers(): Collection
{
$helpers = collect();
foreach (preg_grep('/^([^.])/',scandir(app_path(self::password_helpers))) as $file) {
if (($file === 'Base.php') || (! str_ends_with(strtolower($file),'.php')))
continue;
$class = self::commands.preg_replace('/\.php$/','',$file);
$helpers = $helpers
->merge([$class::id()=>$class]);
}
return $helpers->sort();
}
/**
* Given an LDAP password syntax {xxx}yyyyyy, this function will return the object for xxx
*
* @param string $password
* @return Attribute\Password\Base|null
* @throws \Exception
*/
public static function hash(string $password): ?Attribute\Password\Base
{
$m = [];
preg_match('/^{([A-Z0-9]+)}(.*)$/',$password,$m);
$hash = Arr::get($m,1,'*clear*');
if (($potential=static::helpers()->filter(fn($hasher)=>str_starts_with($hasher::id(),$hash)))->count() > 1) {
foreach ($potential as $item) {
if ($item::subid($password))
return new $item;
}
throw new \Exception(sprintf('Couldnt figure out a password hash for %s',$password));
} elseif (! $potential->count()) {
throw new \Exception(sprintf('Couldnt figure out a password hash for %s',$password));
}
return new ($potential->pop());
}
/**
* Return the object that will process a password
*
* @param string $id
* @return Attribute\Password\Base|null
*/
public static function hash_id(string $id): ?Attribute\Password\Base
{
return ($helpers=static::helpers())->has($id) ? new ($helpers->get($id)) : NULL;
}
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): View
{ {
@ -20,6 +81,23 @@ final class Password extends Attribute
->with('o',$this) ->with('o',$this)
->with('edit',$edit) ->with('edit',$edit)
->with('old',$old) ->with('old',$old)
->with('new',$new); ->with('new',$new)
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key]));
}
public function render_item_old(int $key): ?string
{
$pw = Arr::get($this->oldValues,$key);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
: NULL;
}
public function render_item_new(int $key): ?string
{
$pw = Arr::get($this->values,$key);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
: NULL;
} }
} }

View File

@ -0,0 +1,25 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class Argon2i extends Base
{
public const key = 'ARGON2';
protected const subkey = 'i';
protected const identifier = '$argon2i';
public static function subid(string $password): bool
{
return str_starts_with(base64_decode(self::password($password)),self::identifier.'$');
}
public function compare(string $source,string $compare): bool
{
return password_verify($compare,base64_decode($this->password($source)));
}
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(password_hash($password,PASSWORD_ARGON2I)));
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class Argon2id extends Base
{
public const key = 'ARGON2';
protected const subkey = 'id';
protected const identifier = '$argon2id';
public static function subid(string $password): bool
{
return str_starts_with(base64_decode(self::password($password)),self::identifier.'$');
}
public function compare(string $source,string $compare): bool
{
return password_verify($compare,base64_decode($this->password($source)));
}
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(password_hash($password,PASSWORD_ARGON2ID)));
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
abstract class Base
{
protected const subkey = '';
abstract public function encode(string $password): string;
public static function id(): string
{
return static::key.(static::subkey ? ':'.static::subkey : '');
}
/**
* Remove the hash {TEXT}xxxx from the password
*
* @param string $password
* @return string
*/
protected static function password(string $password): string
{
return preg_replace('/^{'.static::key.'}/','',$password);
}
public static function shortid(): string
{
return static::key;
}
/**
* When multiple passwords share the same ID, this determines which hash is responsible for the presented password
*
* @param string $password
* @return bool
*/
public static function subid(string $password): bool
{
return FALSE;
}
/**
* Compare our password to see if it is the same as that stored
*
* @param string $source Encoded source password
* @param string $compare Password entered by user
* @return bool
*/
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare);
}
protected function salted_hash(string $password,string $algo,int $salt_size=8,string $salt=NULL): string
{
if (is_null($salt))
$salt = hex2bin(random_salt($salt_size));
return base64_encode(hash($algo,$password.$salt,true).$salt);
}
protected function salted_salt(string $source): string
{
$hash = base64_decode(substr($source,strlen(static::key)+2));
return substr($hash,strlen($hash)-static::salt/2);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class Bcrypt extends Base
{
public const key = 'BCRYPT';
private const options = [
'cost' => 8,
];
public function compare(string $source,string $compare): bool
{
return password_verify($compare,base64_decode($this->password($source)));
}
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(password_hash($password,PASSWORD_BCRYPT,self::options)));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class Clear extends Base
{
public const key = '*clear*';
public function encode(string $password): string
{
return $password;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class MD5 extends Base
{
public const key = 'MD5';
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(hash('md5',$password,true)));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SHA extends Base
{
public const key = 'SHA';
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(hash('sha1',$password,true)));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SHA256 extends Base
{
public const key = 'SHA256';
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(hash('sha256',$password,true)));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SHA384 extends Base
{
public const key = 'SHA384';
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(hash('sha384',$password,true)));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SHA512 extends Base
{
public const key = 'SHA512';
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(hash('sha512',$password,true)));
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SMD5 extends Base
{
public const key = 'SMD5';
protected const salt = 8;
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,string $salt=NULL): string
{
if (is_null($salt))
$salt = hex2bin(random_salt(self::salt));
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'md5',self::salt,$salt));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SSHA extends Base
{
public const key = 'SSHA';
protected const salt = 8;
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,string $salt=NULL): string
{
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SSHA256 extends Base
{
public const key = 'SSHA256';
protected const salt = 8;
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,string $salt=NULL): string
{
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha256',self::salt,$salt));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SSHA384 extends Base
{
public const key = 'SSHA384';
protected const salt = 8;
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,string $salt=NULL): string
{
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha384',self::salt,$salt));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Classes\LDAP\Attribute\Password;
final class SSHA512 extends Base
{
public const key = 'SSHA512';
protected const salt = 8;
public function compare(string $source,string $compare): bool
{
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,string $salt=NULL): string
{
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha512',self::salt,$salt));
}
}

View File

@ -324,7 +324,7 @@ final class AttributeType extends Base {
*/ */
public function addRequiredByObjectClass(string $name): void public function addRequiredByObjectClass(string $name): void
{ {
if ($this->required_by_object_classes->search($name) === FALSE) if (! $this->required_by_object_classes->contains($name))
$this->required_by_object_classes->push($name); $this->required_by_object_classes->push($name);
} }
@ -336,7 +336,7 @@ final class AttributeType extends Base {
*/ */
public function addUsedInObjectClass(string $name): void public function addUsedInObjectClass(string $name): void
{ {
if ($this->used_in_object_classes->search($name) === FALSE) if (! $this->used_in_object_classes->contains($name))
$this->used_in_object_classes->push($name); $this->used_in_object_classes->push($name);
} }

View File

@ -115,7 +115,7 @@ final class MatchingRule extends Base {
{ {
$name = trim($name); $name = trim($name);
if ($this->used_by_attrs->search($name) === FALSE) if (! $this->used_by_attrs->contains($name))
$this->used_by_attrs->push($name); $this->used_by_attrs->push($name);
} }

View File

@ -241,7 +241,7 @@ final class ObjectClass extends Base
*/ */
public function addChildObjectClass(string $name): void public function addChildObjectClass(string $name): void
{ {
if (! $this->child_objectclasses->has($name)) if (! $this->child_objectclasses->contains($name))
$this->child_objectclasses->push($name); $this->child_objectclasses->push($name);
} }

View File

@ -90,6 +90,25 @@ class HomeController extends Controller
return $x->render(); return $x->render();
} }
public function entry_password_check(Request $request)
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
$password = $o->getObject('userpassword');
$result = collect();
foreach ($password as $key => $value) {
$hash = $password->hash($value);
$compare = Arr::get($request->password,$key);
//Log::debug(sprintf('comparing [%s] with [%s] type [%s]',$value,$compare,$hash::id()),['object'=>$hash]);
$result->push((($compare !== NULL) && $hash->compare($value,$compare)) ? 'OK' :'FAIL');
}
return $result;
}
/** /**
* Show a confirmation to update a DN * Show a confirmation to update a DN
* *
@ -103,8 +122,24 @@ class HomeController extends Controller
$o = config('server')->fetch($dn); $o = config('server')->fetch($dn);
foreach ($request->except(['_token','dn']) as $key => $value) foreach ($request->except(['_token','dn','userpassword_hash','userpassword']) as $key => $value)
$o->{$key} = array_filter($value); $o->{$key} = array_filter($value,fn($item)=>! is_null($item));
// We need to process and encrypt the password
$passwords = [];
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($o->userpassword,$key)) && ($value === md5($old))) {
array_push($passwords,$old);
continue;
}
if ($value) {
$type = Arr::get($request->userpassword_hash,$key);
array_push($passwords,Attribute\Password::hash_id($type)->encode($value));
}
}
$o->userpassword = $passwords;
if (! $o->getDirty()) if (! $o->getDirty())
return back() return back()
@ -170,8 +205,7 @@ class HomeController extends Controller
return Redirect::to('/') return Redirect::to('/')
->withInput() ->withInput()
->with('success',__('Entry updated')) ->with('updated',collect($dirty)->map(fn($key,$item)=>$o->getObject($item)));
->with('updated',$dirty);
} }
/** /**

View File

@ -32,7 +32,7 @@ class Entry extends Model
parent::discardChanges(); parent::discardChanges();
// If we are discharging changes, we need to reset our $objects; // If we are discharging changes, we need to reset our $objects;
$this->objects = $this->getAttributesAsObjects($this->attributes); $this->objects = $this->getAttributesAsObjects();
return $this; return $this;
} }
@ -45,28 +45,26 @@ class Entry extends Model
*/ */
public function getAttributes(): array public function getAttributes(): array
{ {
return $this->objects->map(function($item) { return $item->values->toArray(); })->toArray(); return $this->objects
->map(fn($item)=>$item->values->toArray())
->toArray();
} }
/** /**
* Determine if the new and old values for a given key are equivalent. * Determine if the new and old values for a given key are equivalent.
*
* @todo This function barfs on language tags, eg: key = givenname;lang-ja
*/ */
protected function originalIsEquivalent(string $key): bool protected function originalIsEquivalent(string $key): bool
{ {
$key = $this->normalizeAttributeKey($key); $key = $this->normalizeAttributeKey($key);
if ((! array_key_exists($key, $this->original)) && (! $this->objects->has($key))) { // @todo Silently ignore keys of language tags - we should work with them
if (str_contains($key,';'))
return TRUE; return TRUE;
}
$current = $this->attributes[$key]; return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($key)))
$original = $this->objects->get($key)->values; || (! $this->getObject($key)->isDirty());
if ($current === $original) {
return true;
}
return ! $this->getObject($key)->isDirty();
} }
public static function query(bool $noattrs=false): Builder public static function query(bool $noattrs=false): Builder
@ -120,7 +118,7 @@ class Entry extends Model
// We only set our objects on DN entries (otherwise we might get into a recursion loop if this is the schema DN) // We only set our objects on DN entries (otherwise we might get into a recursion loop if this is the schema DN)
if ($this->dn && (! in_array($this->dn,Arr::get($this->attributes,'subschemasubentry',[])))) { if ($this->dn && (! in_array($this->dn,Arr::get($this->attributes,'subschemasubentry',[])))) {
$this->objects = $this->getAttributesAsObjects($this->attributes); $this->objects = $this->getAttributesAsObjects();
} else { } else {
$this->objects = collect(); $this->objects = collect();
@ -134,8 +132,8 @@ class Entry extends Model
/** /**
* Return a key to use for sorting * Return a key to use for sorting
* *
* @todo This should be the DN in reverse order
* @return string * @return string
* @todo This should be the DN in reverse order
*/ */
public function getSortKeyAttribute(): string public function getSortKeyAttribute(): string
{ {
@ -148,7 +146,7 @@ class Entry extends Model
{ {
$key = $this->normalizeAttributeKey($key); $key = $this->normalizeAttributeKey($key);
if (config('server')->schema('attributetypes')->has($key) === FALSE) if (! config('server')->schema('attributetypes')->contains($key))
throw new AttributeException('Schema doesnt have attribute [%s]',$key); throw new AttributeException('Schema doesnt have attribute [%s]',$key);
if ($x=$this->objects->get($key)) { if ($x=$this->objects->get($key)) {
@ -165,11 +163,11 @@ class Entry extends Model
* @param array $attributes * @param array $attributes
* @return Collection * @return Collection
*/ */
protected function getAttributesAsObjects(array $attributes): Collection public function getAttributesAsObjects(): Collection
{ {
$result = collect(); $result = collect();
foreach ($attributes as $attribute => $value) { foreach ($this->attributes as $attribute => $value) {
// If the attribute name has language tags // If the attribute name has language tags
$matches = []; $matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) { if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
@ -199,10 +197,10 @@ class Entry extends Model
} }
} }
$sort = collect(config('ldap.attr_display_order',[]))->transform(function($item) { return strtolower($item); }); $sort = collect(config('ldap.attr_display_order',[]))->map(fn($item)=>strtolower($item));
// Order the attributes // Order the attributes
$result = $result->sortBy([function(Attribute $a,Attribute $b) use ($sort): int { return $result->sortBy([function(Attribute $a,Attribute $b) use ($sort): int {
if ($a === $b) if ($a === $b)
return 0; return 0;
@ -225,9 +223,7 @@ 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;
} ]); }]);
return $result;
} }
/** /**
@ -261,9 +257,8 @@ class Entry extends Model
*/ */
public function getInternalAttributes(): Collection public function getInternalAttributes(): Collection
{ {
return $this->objects->filter(function($item) { return $this->objects
return $item->is_internal; ->filter(fn($item)=>$item->is_internal);
});
} }
/** /**
@ -274,14 +269,15 @@ class Entry extends Model
*/ */
public function getObject(string $key): Attribute|null public function getObject(string $key): Attribute|null
{ {
return $this->objects->get($this->normalizeAttributeKey($key)); return $this->objects
->get($this->normalizeAttributeKey($key));
} }
public function getObjects(): Collection public function getObjects(): Collection
{ {
// In case we havent built our objects yet (because they werent available while determining the schema DN) // In case we havent built our objects yet (because they werent available while determining the schema DN)
if ((! $this->objects->count()) && $this->attributes) if ((! $this->objects->count()) && $this->attributes)
$this->objects = $this->getAttributesAsObjects($this->attributes); $this->objects = $this->getAttributesAsObjects();
return $this->objects; return $this->objects;
} }
@ -293,7 +289,8 @@ class Entry extends Model
*/ */
public function getMissingAttributes(): Collection public function getMissingAttributes(): Collection
{ {
return $this->getAvailableAttributes()->diff($this->getVisibleAttributes()); return $this->getAvailableAttributes()
->diff($this->getVisibleAttributes());
} }
/** /**
@ -303,14 +300,14 @@ class Entry extends Model
*/ */
public function getVisibleAttributes(): Collection public function getVisibleAttributes(): Collection
{ {
return $this->objects->filter(function($item) { return $this->objects
return ! $item->is_internal; ->filter(fn($item)=>! $item->is_internal);
});
} }
public function hasAttribute(int|string $key): bool public function hasAttribute(int|string $key): bool
{ {
return $this->objects->has($key); return $this->objects
->has($key);
} }
/** /**

View File

@ -11,9 +11,6 @@ trait MD5Updates
{ {
public function isDirty(): bool public function isDirty(): bool
{ {
if (! parent::isDirty())
return TRUE;
foreach ($this->values->diff($this->oldValues) as $key => $value) foreach ($this->values->diff($this->oldValues) as $key => $value)
if (md5(Arr::get($this->oldValues,$key)) !== $value) if (md5(Arr::get($this->oldValues,$key)) !== $value)
return TRUE; return TRUE;

View File

@ -8,22 +8,22 @@ use App\Classes\LDAP\Attribute as LDAPAttribute;
class Attribute extends Component class Attribute extends Component
{ {
public LDAPAttribute $o; public ?LDAPAttribute $o;
public bool $edit; public bool $edit;
public bool $new; public bool $new;
public bool $old; public bool $old;
public ?string $na;
/** /**
* Create a new component instance. * Create a new component instance.
*
* @return void
*/ */
public function __construct(LDAPAttribute $o,bool $edit,bool $old=FALSE,bool $new=FALSE) public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $na=NULL)
{ {
$this->o = $o; $this->o = $o;
$this->edit = $edit; $this->edit = $edit;
$this->old = $old; $this->old = $old;
$this->new = $new; $this->new = $new;
$this->na = $na;
} }
/** /**
@ -33,6 +33,9 @@ class Attribute extends Component
*/ */
public function render() public function render()
{ {
return $this->o->render($this->edit,$this->old,$this->new); return $this->o
? $this->o
->render($this->edit,$this->old,$this->new)
: $this->na;
} }
} }

View File

@ -10,4 +10,23 @@ function login_attr_description(): string
function login_attr_name(): string function login_attr_name(): string
{ {
return key(config('ldap.login.attr')); return key(config('ldap.login.attr'));
}
/**
* Used to generate a random salt for crypt-style passwords. Salt strings are used
* to make pre-built hash cracking dictionaries difficult to use as the hash algorithm uses
* not only the user's password but also a randomly generated string. The string is
* stored as the first N characters of the hash for reference of hashing algorithms later.
*
* @param int $length The length of the salt string to generate.
* @return string The generated salt string.
* @throws \Random\RandomException
*/
function random_salt(int $length): string
{
$str = bin2hex(random_bytes(ceil($length/2)));
if ($length%2 === 1)
return substr($str,0,-1);
return $str;
} }

View File

@ -153,13 +153,25 @@ return [
'mail' => [ 'mail' => [
'mail'=>[ 'mail'=>[
'sometimes', 'sometimes',
'array','min:1' 'array',
'min:1'
], ],
'mail.*' => [ 'mail.*' => [
'nullable', 'nullable',
'email' 'email'
] ]
], ],
'userpassword' => [
'userpassword' => [
'sometimes',
'array',
'min:1'
],
'userpassword.*' => [
'nullable',
'min:8'
]
],
'uidnumber' => [ 'uidnumber' => [
'uidnumber' => [ 'uidnumber' => [
'sometimes', 'sometimes',

View File

@ -670,7 +670,7 @@ function get_request($attr,$type='POST',$die=false,$default=null,$preventXSS=tru
$value = isset($_POST[$attr]) ? (is_array($_POST[$attr]) ? $_POST[$attr] : (empty($_POST['nodecode'][$attr]) ? rawurldecode($_POST[$attr]) : $_POST[$attr])) : $default; $value = isset($_POST[$attr]) ? (is_array($_POST[$attr]) ? $_POST[$attr] : (empty($_POST['nodecode'][$attr]) ? rawurldecode($_POST[$attr]) : $_POST[$attr])) : $default;
break; break;
} }
if ($die && is_null($value)) if ($die && is_null($value))
system_message(array( system_message(array(
'title'=>_('Generic Error'), 'title'=>_('Generic Error'),
@ -1757,32 +1757,6 @@ function expand_dn_with_base($base,$sub_dn) {
return sprintf('%s%s',$sub_dn,$base); return sprintf('%s%s',$sub_dn,$base);
} }
/**
* Used to generate a random salt for crypt-style passwords. Salt strings are used
* to make pre-built hash cracking dictionaries difficult to use as the hash algorithm uses
* not only the user's password but also a randomly generated string. The string is
* stored as the first N characters of the hash for reference of hashing algorithms later.
*
* @param int The length of the salt string to generate.
* @return string The generated salt string.
*/
function random_salt($length) {
if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
debug_log('Entered (%%)',1,0,__FILE__,__LINE__,__METHOD__,$fargs);
$possible = '0123456789'.
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'./';
$str = '';
mt_srand((double)microtime() * 1000000);
while (strlen($str) < $length)
$str .= substr($possible,(rand()%strlen($possible)),1);
return $str;
}
/** /**
* Split an RDN into its attributes * Split an RDN into its attributes
*/ */
@ -1999,368 +1973,6 @@ function draw_jpeg_photo($server,$dn,$attr_name='jpegphoto',$index,$draw_delete_
$attr_name,_('Delete photo')); $attr_name,_('Delete photo'));
} }
/**
* Return the list of available password types
*
* @todo Dynamically work this list out so we only present hashes that we can encrypt
*/
function password_types() {
if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
debug_log('Entered (%%)',1,0,__FILE__,__LINE__,__METHOD__,$fargs);
return array(
''=>'clear',
'bcrypt'=>'bcrypt',
'blowfish'=>'blowfish',
'crypt'=>'crypt',
'ext_des'=>'ext_des',
'md5'=>'md5',
'k5key'=>'k5key',
'md5crypt'=>'md5crypt',
'sha'=>'sha',
'smd5'=>'smd5',
'ssha'=>'ssha',
'sha512'=>'sha512',
'sha256crypt'=>'sha256crypt',
'sha512crypt'=>'sha512crypt',
);
}
/**
* Hashes a password and returns the hash based on the specified enc_type.
*
* @param string The password to hash in clear text.
* @param string Standard LDAP encryption type which must be one of
* crypt, ext_des, md5crypt, blowfish, md5, sha, smd5, ssha, sha512,
* sha256crypt, sha512crypt, or clear.
* @return string The hashed password.
*/
function pla_password_hash($password_clear,$enc_type) {
if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
debug_log('Entered (%%)',1,0,__FILE__,__LINE__,__METHOD__,$fargs);
$enc_type = strtolower($enc_type);
switch($enc_type) {
case 'blowfish':
if (! defined('CRYPT_BLOWFISH') || CRYPT_BLOWFISH == 0)
error(_('Your system crypt library does not support blowfish encryption.'),'error','index.php');
# Hardcoded to second blowfish version and set number of rounds
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,'$2a$12$'.random_salt(13)));
break;
case 'crypt':
if ($_SESSION[APPCONFIG]->getValue('password', 'no_random_crypt_salt'))
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,substr($password_clear,0,2)));
else
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,random_salt(2)));
break;
case 'ext_des':
# Extended des crypt. see OpenBSD crypt man page.
if (! defined('CRYPT_EXT_DES') || CRYPT_EXT_DES == 0)
error(_('Your system crypt library does not support extended DES encryption.'),'error','index.php');
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,'_'.random_salt(8)));
break;
case 'k5key':
$new_value = sprintf('{K5KEY}%s',$password_clear);
system_message(array(
'title'=>_('Unable to Encrypt Password'),
'body'=>'phpLDAPadmin cannot encrypt K5KEY passwords',
'type'=>'warn'));
break;
case 'md5':
$new_value = sprintf('{MD5}%s',base64_encode(pack('H*',md5($password_clear))));
break;
case 'md5crypt':
if (! defined('CRYPT_MD5') || CRYPT_MD5 == 0)
error(_('Your system crypt library does not support md5crypt encryption.'),'error','index.php');
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,'$1$'.random_salt(9)));
break;
case 'sha':
# Use php 4.3.0+ sha1 function, if it is available.
if (function_exists('sha1'))
$new_value = sprintf('{SHA}%s',base64_encode(pack('H*',sha1($password_clear))));
elseif (function_exists('mhash'))
$new_value = sprintf('{SHA}%s',base64_encode(mhash(MHASH_SHA1,$password_clear)));
else
error(_('Your PHP install does not have the mhash() function. Cannot do SHA hashes.'),'error','index.php');
break;
case 'ssha':
if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
mt_srand((double)microtime()*1000000);
$salt = mhash_keygen_s2k(MHASH_SHA1,$password_clear,substr(pack('h*',md5(mt_rand())),0,8),4);
$new_value = sprintf('{SSHA}%s',base64_encode(mhash(MHASH_SHA1,$password_clear.$salt).$salt));
} else {
error(_('Your PHP install does not have the mhash() or mhash_keygen_s2k() function. Cannot do S2K hashes.'),'error','index.php');
}
break;
case 'bcrypt':
$options = [
'cost' => 8,
];
#Checking if password_hash() function is available.
if (function_exists('password_hash'))
$new_value = sprintf('{BCRYPT}%s',base64_encode(password_hash($password_clear, PASSWORD_BCRYPT, $options)));
else
error(_('Your PHP install does not have the password_hash() function. Cannot do BCRYPT hashes.'),'error','index.php');
break;
case 'smd5':
if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
mt_srand((double)microtime()*1000000);
$salt = mhash_keygen_s2k(MHASH_MD5,$password_clear,substr(pack('h*',md5(mt_rand())),0,8),4);
$new_value = sprintf('{SMD5}%s',base64_encode(mhash(MHASH_MD5,$password_clear.$salt).$salt));
} else {
error(_('Your PHP install does not have the mhash() or mhash_keygen_s2k() function. Cannot do S2K hashes.'),'error','index.php');
}
break;
case 'sha512':
if (function_exists('openssl_digest') && function_exists('base64_encode')) {
$new_value = sprintf('{SHA512}%s', base64_encode(openssl_digest($password_clear, 'sha512', true)));
} else {
error(_('Your PHP install doest not have the openssl_digest() or base64_encode() function. Cannot do SHA512 hashes. '),'error','index.php');
}
break;
case 'sha256crypt':
if (! defined('CRYPT_SHA256') || CRYPT_SHA256 == 0)
error(_('Your system crypt library does not support sha256crypt encryption.'),'error','index.php');
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,'$5$'.random_salt(8)));
break;
case 'sha512crypt':
if (! defined('CRYPT_SHA512') || CRYPT_SHA512 == 0)
error(_('Your system crypt library does not support sha512crypt encryption.'),'error','index.php');
$new_value = sprintf('{CRYPT}%s',crypt($password_clear,'$6$'.random_salt(8)));
break;
case 'clear':
default:
$new_value = $password_clear;
}
return $new_value;
}
/**
* Given a clear-text password and a hash, this function determines if the clear-text password
* is the password that was used to generate the hash. This is handy to verify a user's password
* when all that is given is the hash and a "guess".
* @param String The hash.
* @param String The password in clear text to test.
* @return Boolean True if the clear password matches the hash, and false otherwise.
*/
function password_check($cryptedpassword,$plainpassword,$attribute='userpassword') {
$plainpassword = htmlspecialchars_decode($plainpassword);
if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
debug_log('Entered (%%)',1,0,__FILE__,__LINE__,__METHOD__,$fargs);
if (in_array($attribute,array('sambalmpassword','sambantpassword'))) {
$smb = new smbHash;
switch($attribute) {
case 'sambalmpassword':
if (strcmp($smb->lmhash($plainpassword),strtoupper($cryptedpassword)) == 0)
return true;
else
return false;
case 'sambantpassword':
if (strcmp($smb->nthash($plainpassword),strtoupper($cryptedpassword)) == 0)
return true;
else
return false;
}
return false;
}
if (preg_match('/{([^}]+)}(.*)/',$cryptedpassword,$matches)) {
$cryptedpassword = $matches[2];
$cypher = strtolower($matches[1]);
} else {
$cypher = null;
}
switch($cypher) {
# SSHA crypted passwords
case 'ssha':
# Check php mhash support before using it
if (function_exists('mhash')) {
$hash = base64_decode($cryptedpassword);
# OpenLDAP uses a 4 byte salt, SunDS uses an 8 byte salt - both from char 20.
$salt = substr($hash,20);
$new_hash = base64_encode(mhash(MHASH_SHA1,$plainpassword.$salt).$salt);
if (strcmp($cryptedpassword,$new_hash) == 0)
return true;
else
return false;
} else {
error(_('Your PHP install does not have the mhash() function. Cannot do SHA hashes.'),'error','index.php');
}
break;
#BCRYPT hashed passwords
case 'bcrypt':
# Check php password_verify support before using it
if (function_exists('password_verify')) {
$hash = base64_decode($cryptedpassword);
if (password_verify($plainpassword, $hash)) {
return true;
} else {
return false;
}
} else {
error(_('Your PHP install does not have the password_verify() function. Cannot do Bcrypt hashes.'),'error','index.php');
}
break;
# Salted MD5
case 'smd5':
# Check php mhash support before using it
if (function_exists('mhash')) {
$hash = base64_decode($cryptedpassword);
$salt = substr($hash,16);
$new_hash = base64_encode(mhash(MHASH_MD5,$plainpassword.$salt).$salt);
if (strcmp($cryptedpassword,$new_hash) == 0)
return true;
else
return false;
} else {
error(_('Your PHP install does not have the mhash() function. Cannot do SHA hashes.'),'error','index.php');
}
break;
# SHA crypted passwords
case 'sha':
if (strcasecmp(pla_password_hash($plainpassword,'sha'),'{SHA}'.$cryptedpassword) == 0)
return true;
else
return false;
break;
# MD5 crypted passwords
case 'md5':
if( strcasecmp(pla_password_hash($plainpassword,'md5'),'{MD5}'.$cryptedpassword) == 0)
return true;
else
return false;
break;
# Crypt passwords
case 'crypt':
# Check if it's blowfish crypt
if (preg_match('/^\\$2+/',$cryptedpassword)) {
# Make sure that web server supports blowfish crypt
if (! defined('CRYPT_BLOWFISH') || CRYPT_BLOWFISH == 0)
error(_('Your system crypt library does not support blowfish encryption.'),'error','index.php');
list($version,$rounds,$salt_hash) = explode('$',$cryptedpassword);
if (crypt($plainpassword,'$'.$version.'$'.$rounds.'$'.$salt_hash) == $cryptedpassword)
return true;
else
return false;
}
# Check if it's an crypted md5
elseif (strstr($cryptedpassword,'$1$')) {
# Make sure that web server supports md5 crypt
if (! defined('CRYPT_MD5') || CRYPT_MD5 == 0)
error(_('Your system crypt library does not support md5crypt encryption.'),'error','index.php');
list($dummy,$type,$salt,$hash) = explode('$',$cryptedpassword);
if (crypt($plainpassword,'$1$'.$salt) == $cryptedpassword)
return true;
else
return false;
}
# Check if it's extended des crypt
elseif (strstr($cryptedpassword,'_')) {
# Make sure that web server supports ext_des
if (! defined('CRYPT_EXT_DES') || CRYPT_EXT_DES == 0)
error(_('Your system crypt library does not support extended DES encryption.'),'error','index.php');
if (crypt($plainpassword,$cryptedpassword) == $cryptedpassword)
return true;
else
return false;
}
# Password is plain crypt
else {
if (crypt($plainpassword,$cryptedpassword) == $cryptedpassword)
return true;
else
return false;
}
break;
# SHA512 crypted passwords
case 'sha512':
if (strcasecmp(pla_password_hash($plainpassword,'sha512'),'{SHA512}'.$cryptedpassword) == 0)
return true;
else
return false;
break;
# No crypt is given assume plaintext passwords are used
default:
if ($plainpassword == $cryptedpassword)
return true;
else
return false;
}
}
/** /**
* Detects password encryption type * Detects password encryption type
* *

12
package-lock.json generated
View File

@ -20,7 +20,8 @@
"resolve-url-loader": "^5.0.0", "resolve-url-loader": "^5.0.0",
"sass": "^1.58.3", "sass": "^1.58.3",
"sass-loader": "^13.2.0", "sass-loader": "^13.2.0",
"select2": "^4.1.0-rc.0" "select2": "^4.1.0-rc.0",
"select2-bootstrap-5-theme": "^1.3.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -7644,6 +7645,15 @@
"resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz", "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz",
"integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==" "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A=="
}, },
"node_modules/select2-bootstrap-5-theme": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/select2-bootstrap-5-theme/-/select2-bootstrap-5-theme-1.3.0.tgz",
"integrity": "sha512-uEJDruP4tmwyKcs3V0oP7CIsyC45PGF5ddo8unwTp8OucJ1PSuTOBr+xbWaHTQPGnvp7N96psVQ5UBMQvFCcHA==",
"license": "MIT",
"dependencies": {
"bootstrap": "^5.1.3"
}
},
"node_modules/selfsigned": { "node_modules/selfsigned": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",

View File

@ -25,6 +25,7 @@
"resolve-url-loader": "^5.0.0", "resolve-url-loader": "^5.0.0",
"sass": "^1.58.3", "sass": "^1.58.3",
"sass-loader": "^13.2.0", "sass-loader": "^13.2.0",
"select2": "^4.1.0-rc.0" "select2": "^4.1.0-rc.0",
"select2-bootstrap-5-theme": "^1.3.0"
} }
} }

10
public/css/custom.css vendored
View File

@ -2,4 +2,14 @@ img.jpegphoto {
display:block; display:block;
max-width:100px; max-width:100px;
height:100px; height:100px;
}
/** ensure our userpassword has select is next to the password input */
div#userpassword .select2-container--bootstrap-5 .select2-selection {
font-size: inherit;
border-bottom-right-radius: unset;
border-top-right-radius: unset;
width: 9em;
border: #444054 1px solid;
background-color: #f0f0f0;
} }

22
public/css/fixes.css vendored
View File

@ -268,4 +268,26 @@ pre code .line::before {
opacity: 0.5; opacity: 0.5;
display: inline-block; display: inline-block;
border-right: 1px solid rgba(0, 0, 0, .5); border-right: 1px solid rgba(0, 0, 0, .5);
}
/** select2 rendering fixes */
/* The opened input box */
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
line-height: 1.0;
border: 1px solid #aaa;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.3rem 0.2rem 0.3rem 0.4rem;
font-size: 95%;
}
select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear, .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear {
width: 0.5rem;
height: 0.5rem;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder {
line-height: 1.0;
font-size: 90%;
} }

View File

@ -6,3 +6,4 @@
// Select2 // Select2
@import "select2/dist/css/select2"; @import "select2/dist/css/select2";
@import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme";

View File

@ -11,14 +11,7 @@
<div class="h-100 bg-animation"> <div class="h-100 bg-animation">
<div class="d-flex h-100 justify-content-center align-items-center"> <div class="d-flex h-100 justify-content-center align-items-center">
<div class="mx-auto app-login-box col-md-8"> <div class="mx-auto app-login-box col-md-8">
@if(file_exists('login-note.html')) <x-file-note file="login-note.html"/>
<div class="mx-auto card text-white card-body bg-primary w-50">
<h5 class="text-white card-title"><i class="icon fa-2x fas fa-info pe-3"></i><span class="font-size-xlg">NOTE</span></h5>
<span class="w-100 pb-0">
{!! file_get_contents('login-note.html') !!}
</span>
</div>
@endif
<div class="modal-dialog w-100 mx-auto"> <div class="modal-dialog w-100 mx-auto">
<div class="modal-content"> <div class="modal-content">

View File

@ -27,7 +27,7 @@
<div tabindex="-1" role="menu" aria-hidden="true" class="dropdown-menu dropdown-menu-right"> <div tabindex="-1" role="menu" aria-hidden="true" class="dropdown-menu dropdown-menu-right">
<ul class="nav flex-column"> <ul class="nav flex-column">
@if ((isset($page_actions) && ($page_actions->search('edit') !== FALSE)) || old()) @if ((isset($page_actions) && $page_actions->contains('edit')) || old())
<li class="nav-item"> <li class="nav-item">
<span class="nav-link pt-0 pb-1"> <span class="nav-link pt-0 pb-1">
<button id="entry-edit" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start"> <button id="entry-edit" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start">
@ -37,7 +37,7 @@
</li> </li>
@endif @endif
@if (isset($page_actions) && ($page_actions->search('export') !== FALSE)) @if (isset($page_actions) && $page_actions->contains('export'))
<li class="nav-item"> <li class="nav-item">
<a class="nav-link pt-0 pb-1"> <a class="nav-link pt-0 pb-1">
<button type="button" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start" data-bs-toggle="modal" data-bs-target="#entry-export-modal" {{--data-bs-whatever="ldif"--}}> <button type="button" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start" data-bs-toggle="modal" data-bs-target="#entry-export-modal" {{--data-bs-whatever="ldif"--}}>
@ -47,7 +47,7 @@
</li> </li>
@endif @endif
@if (isset($page_actions) && ($page_actions->search('copy') !== FALSE)) @if (isset($page_actions) && $page_actions->contains('copy'))
<li class="nav-item"> <li class="nav-item">
<a class="nav-link pt-0 pb-1"> <a class="nav-link pt-0 pb-1">
<button class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start"> <button class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start">

View File

@ -33,6 +33,8 @@
<link rel="stylesheet" href="{{ asset('/css/custom.css') }}"> <link rel="stylesheet" href="{{ asset('/css/custom.css') }}">
@endif @endif
<!-- Page Styles -->
@yield('page-styles')
{{-- {{--
@if(file_exists('css/print.css')) @if(file_exists('css/print.css'))
<!-- Printing Modifications --> <!-- Printing Modifications -->

View File

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

View File

@ -1,9 +1,9 @@
<!-- $o=Attribute::class --> <!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o"> <x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach (old($o->name_lc,$new ? [NULL] : $o->values) as $value) @foreach(old($o->name_lc,($new ?? FALSE) ? [NULL] : $o->values) as $value)
@if ($edit && ! $o->is_rdn) @if (($edit ?? FALSE) && ! $o->is_rdn)
<div class="input-group has-validation"> <div class="input-group has-validation">
<input type="text" 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="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" @if (! $new)readonly="true" @endif"> <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"> <div class="invalid-feedback pb-2">
@if($e) @if($e)

View File

@ -9,7 +9,7 @@
@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 @if($e=$errors->get($o->name_lc.'.'.$loop->index))is-invalid @endif" src="data:{{ $x }};base64, {{ base64_encode($value) }}" /> <img @class(['jpegphoto','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index))]) src="data:{{ $x }};base64, {{ base64_encode($value) }}" />
@if ($edit) @if ($edit)
<br> <br>

View File

@ -1,10 +1,10 @@
<div class="row pt-2"> <div class="row pt-2">
<div class="col-1 @if(! $edit)d-none @endif"></div> <div @class(['col-1','d-none'=>(! $edit)])></div>
<div class="col-10 p-2"> <div class="col-10 p-2">
<div id="{{ $o->name_lc }}"> <div id="{{ $o->name_lc }}">
{{ $slot }} {{ $slot }}
</div> </div>
<x-attribute.widget.options :o="$o" :edit="$edit" :new="$new"></x-attribute.widget.options> <x-attribute.widget.options :o="$o" :edit="$edit" :new="$new"/>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
@foreach (old($o->name_lc,$o->values) as $value) @foreach (old($o->name_lc,$o->values) as $value)
@if ($edit && ($value === NULL || (! $o->isStructural($value)))) @if ($edit && ($value === NULL || (! $o->isStructural($value))))
<div class="input-group has-validation"> <div class="input-group has-validation">
<input type="text" 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="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" readonly="true"> <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(true)>
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)
{{ join('|',$e) }} {{ join('|',$e) }}
@ -14,7 +14,7 @@
{{ $value }} {{ $value }}
@if ($o->isStructural($value)) @if ($o->isStructural($value))
<input type="hidden" name="{{ $o->name_lc }}[]" value="{{ $value }}"> <input type="hidden" name="{{ $o->name_lc }}[]" value="{{ $value }}">
<span class="float-end mb-2">@lang('structural')</span> <span class="float-end">@lang('structural')</span>
@endif @endif
<br> <br>
@endif @endif

View File

@ -1,10 +1,10 @@
<!-- @todo We are not handling redirect backs with updated values -->
<!-- $o=Password::class --> <!-- $o=Password::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o"> <x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach ($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 mb-3">
<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"> <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"> <div class="invalid-feedback pb-2">
@if($e) @if($e)
@ -13,11 +13,17 @@
</div> </div>
</div> </div>
@else @else
{{ $value }}<br> {{ (($x=$o->hash($value)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '' }}{{ str_repeat('*',16) }}
@endif @endif
@endforeach @endforeach
</x-attribute.layout>
<span class="p-0 m-0"> @if($edit)
<span class="btn btn-sm btn-outline-dark mt-3"><i class="fas fa-user-check"></i> @lang('Check Password')</span> <div class="row">
</span> <div class="offset-1 col-4 p-2">
</x-attribute.layout> <span class="p-0 m-0">
<button type="button" class="btn btn-transition btn-sm btn-outline-dark mt-3" data-bs-toggle="modal" data-bs-target="#userpassword-check-modal"><i class="fas fa-user-check"></i> @lang('Check Password')</button>
</span>
</div>
</div>
@endif

View File

@ -2,30 +2,18 @@
<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)
<span 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','d-none'=>(! $new)]) id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
@if($new)
<script type="text/javascript">
$(document).ready(function() {
// Create a new entry when Add Value clicked
$('#{{ $o->name_lc }}.addable').click(function (item) {
var cln = $(this).parent().parent().find('input:last').clone();
cln.val('').attr('placeholder', '[@lang('NEW')]');
cln.appendTo('#' + item.currentTarget.id)
});
});
</script>
@endif
</span> </span>
@endif @endif
@section('page-scripts') @section('page-scripts')
@if(($edit && $o->can_addvalues)) @if($edit && $o->can_addvalues)
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// Create a new entry when Add Value clicked // Create a new entry when Add Value clicked
$('#{{ $o->name_lc }}.addable').click(function (item) { $('#{{ $o->name_lc }}.addable').click(function (item) {
var cln = $(this).parent().parent().find('input:last').clone(); var cln = $(this).parent().parent().find('input:last').clone();
cln.val('').attr('placeholder', '[@lang('NEW')]'); cln.attr('value','').attr('placeholder', '[@lang('NEW')]');
cln.appendTo('#' + item.currentTarget.id) cln.appendTo('#' + item.currentTarget.id)
}); });
}); });

View File

@ -0,0 +1,11 @@
@if($errors->any())
<div class="alert alert-danger">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-down"></i> Error?</h4>
<hr>
<ul style="list-style-type: square;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

View File

@ -0,0 +1,12 @@
@if(file_exists($file))
<div class="row pb-3">
<div class="col-12">
<div class="mx-auto card text-white card-body bg-primary w-50">
<h5 class="text-white card-title"><i class="icon fa-2x fas fa-info pe-3"></i><span class="font-size-xlg">NOTE</span></h5>
<span class="w-100 pb-0">
{!! file_get_contents($file) !!}
</span>
</div>
</div>
</div>
@endif

View File

@ -0,0 +1,29 @@
<div class="form-group">
@if(isset($label))
<label {{ $attributes->only(['class'])->merge(['class'=>'form-label']) }} for="{{ $id ?? $name }}">{!! html_entity_decode($label) !!}</label>
@endisset
<div class="input-group has-validation">
@if(isset($icon) || isset($prepend))
{{-- // messes with the icon box, we have rounded corners on the right side
<div class="input-group-prepend">
--}}
<span class="input-group-text">@isset($icon)<i class="bi {{ $icon }}"></i>@endisset @isset($prepend){!! $prepend !!}@endisset</span>
{{--
</div>
--}}
@endif
{{ $slot }}
@isset($name)
<span class="invalid-feedback">
@error((! empty($old)) ? $old : $name)
{{ $message }}
@elseif(isset($feedback))
{{ $feedback }}
@enderror
</span>
@endisset
</div>
@isset($helper)
<span class="input-helper">{!! html_entity_decode($helper) !!}</span>
@endif
</div>

View File

@ -0,0 +1,9 @@
<button id="cancel" class="btn btn-sm btn-outline-dark">Cancel</button>
@section('page-scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#cancel').on('click',()=>history.back());
});
</script>
@append

View File

@ -0,0 +1,9 @@
<button id="form-reset" class="btn btn-outline-danger">@lang('Reset')</button>
@section('page-scripts')
<script>
$(document).ready(function() {
$('#form-reset').on('click',()=>$('#{{$form}}')[0].reset());
});
</script>
@append

View File

@ -0,0 +1,63 @@
<x-form.base {{ $attributes }}>
@isset($name)
<input type="hidden" id="{{ $id ?? $name }}_disabled" name="{{ $name }}" value="" disabled>
@endisset
<select style="width: 80%" class="form-select @isset($name)@error((! empty($old)) ? $old : $name) is-invalid @enderror @endisset" id="{{ $id ?? $name }}" @isset($name)name="{{ $name }}"@endisset @required(isset($required) && $required) @disabled(isset($disabled) && $disabled)>
@if(empty($value) || isset($addnew) || isset($choose))
<option value=""></option>
@isset($addnew)
<option value="new">{{ $addnew ?: 'Add New' }}</option>
@endisset
@endif
@isset($options)
@empty($groupby)
@foreach($options as $option)
@continue(! Arr::get($option,'value'))
<option value="{{ Arr::get($option,'id') }}" @selected(isset($name) && (Arr::get($option,'id') == old($old ?? $name,$value ?? '')))>{{ Arr::get($option,'value') }}</option>
@endforeach
@else
@foreach($options->groupBy($groupby) as $group)
<optgroup label="{{ $groupby == 'active' ? (Arr::get($group->first(),$groupby) ? 'Active' : 'Not Active') : Arr::get($group->first(),$groupby) }}">
@foreach($group as $option)
@continue(! Arr::get($option,'value'))
<option value="{{ Arr::get($option,'id') }}" @selected(isset($name) && (Arr::get($option,'id') == old($old ?? $name,$value ?? '')))>{{ Arr::get($option,'value') }}</option>
@endforeach
</optgroup>
@endforeach
@endempty
@endisset
</select>
</x-form.base>
@section('page-scripts')
<script type="text/javascript">
// Select doesnt support read only so we'll use disable and a new field
@isset($name)
function {{$id ?? $name}}_readonly(on) {
if (on) {
$('#{{ $name }}').prop('disabled',true);
$('#{{ $name }}_disabled').prop('disabled',false).val($('#{{ $name }}').val());
} else {
$('#{{ $name }}').prop('disabled',false);
$('#{{ $name }}_disabled').prop('disabled',true);
}
}
@endisset
$(document).ready(function() {
$('#{{ $id ?? $name }}').select2({
theme: 'bootstrap-5',
dropdownAutoWidth: true,
width: 'style',
allowClear: {{ $allowclear ?? 'true' }},
placeholder: '{{ $placeholder ?? '' }}',
@isset($addvalues)
tags: true,
@endisset
});
});
</script>
@append

View File

@ -0,0 +1,9 @@
<button id="form-submit" class="btn btn-sm btn-success">@lang($action)</button>
@section('page-scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#form-submit').on('click',()=>$('#{{$form}}')[0].submit());
});
</script>
@append

View File

@ -0,0 +1,7 @@
@if(session()->has('note'))
<div class="alert alert-info">
<h4 class="alert-heading"><i class="fas fa-fw fa-note-sticky"></i> Note:</h4>
<hr>
<p>{{ session()->pull('note') }}</p>
</div>
@endif

View File

@ -0,0 +1,11 @@
@if(session()->has('success'))
<div class="alert alert-success">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
<hr>
<ul style="list-style-type: square;">
@foreach (session()->get('success') as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
</div>
@endif

View File

@ -0,0 +1,12 @@
@if(session()->has('updated'))
<div class="alert alert-success">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
<hr>
<p>{{ __('Entry updated') }}</p>
<ul style="list-style-type: square;">
@foreach (session()->pull('updated') as $key => $o)
<li><abbr title="{{ $o->description }}">{{ $o->name }}</abbr>: {{ $o->values->map(fn($item,$key)=>$o->render_item_new($key))->join(',') }}</li>
@endforeach
</ul>
</div>
@endif

View File

@ -0,0 +1,32 @@
<table class="table table-borderless">
<tr>
<td class="{{ ($x=$o->getObject('jpegphoto')) ? 'border' : '' }}" rowspan="2">
{!! $x ? $x->render(FALSE,TRUE) : 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>
</tr>
<tr>
<td class="line-height-1" style="font-size: 55%;vertical-align: bottom;" colspan="2">
<table>
<tr>
<td class="p-1 m-1">Created</td>
<th class="p-1 m-1">
<x-attribute :o="$o->getObject('createtimestamp')" :na="__('Unknown')"/> [<x-attribute :o="$o->getObject('creatorsname')" :na="__('Unknown')"/>]
</th>
</tr>
<tr>
<td class="p-1 m-1">Modified</td>
<th class="p-1 m-1">
<x-attribute :o="$o->getObject('modifytimestamp')" :na="__('Unknown')"/> [<x-attribute :o="$o->getObject('modifiersname')" :na="__('Unknown')"/>]
</th>
</tr>
<tr>
<td class="p-1 m-1">UUID</td>
<th class="p-1 m-1">
<x-attribute :o="$o->getObject('entryuuid')" :na="__('Unknown')"/>
</th>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -1,66 +1,15 @@
@extends('layouts.dn') @extends('layouts.dn')
@section('page_title') @section('page_title')
<table class="table table-borderless"> @include('fragment.dn.header')
<tr>
<td class="{{ ($x=$o->getObject('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>
</tr>
<tr>
<td class="line-height-1" style="font-size: 55%;vertical-align: bottom;" colspan="2">
<table>
<tr>
<td class="p-1 m-1">Created</td>
<th class="p-1 m-1">{{ ($x=$o->getObject('createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('creatorsname')) ? $x->render() : __('Unknown') }}]</th>
</tr>
<tr>
<td class="p-1 m-1">Modified</td>
<th class="p-1 m-1">{{ ($x=$o->getObject('modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('modifiersname')) ? $x->render() : __('Unknown') }}]</th>
</tr>
<tr>
<td class="p-1 m-1">UUID</td>
<th class="p-1 m-1">{{ $o->entryuuid[0] ?? '' }}</th>
</tr>
</table>
</td>
</tr>
</table>
@endsection @endsection
@section('main-content') @section('main-content')
@if(session()->has('note')) <x-note/>
<div class="alert alert-info"> <x-updated/>
<h4 class="alert-heading"><i class="fas fa-fw fa-note-sticky"></i> Note:</h4> <x-error/>
<hr>
<p>{{ session()->pull('note') }}</p>
</div>
@endif
@if(session()->has('success'))
<div class="alert alert-success">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
<hr>
<p>{{ session()->pull('success') }}</p>
<ul style="list-style-type: square;">
@foreach (session()->pull('updated') as $key => $values)
<li>{{ $key }}: {{ join(',',$values) }}</li>
@endforeach
</ul>
</div>
@endif
<!-- @todo If we are redirected here, check old() and add back any attributes that were in the original submission --> <!-- @todo If we are redirected here, check old() and add back any attributes that were in the original submission -->
@if($errors->any())
<div class="alert alert-danger">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-down"></i> Error?</h4>
<hr>
<ul style="list-style-type: square;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="main-card mb-3 card"> <div class="main-card mb-3 card">
<div class="card-body"> <div class="card-body">
@ -102,13 +51,7 @@
<div class="row"> <div class="row">
<div class="col-12 pt-2"> <div class="col-12 pt-2">
<label for="newattr" class="form-label">Select from...</label> <x-form.select id="newattr" label="Select from..." :options="$o->getMissingAttributes()->sortBy('name')->map(fn($item)=>['id'=>$item->name,'value'=>$item->name_lc])"/>
<select class="form-select" id="newattr">
<option value="">&nbsp;</option>
@foreach ($o->getMissingAttributes() as $ao)
<option value="{{ $ao->name_lc }}">{{ $ao->name }}</option>
@endforeach
</select>
</div> </div>
</div> </div>
@endif @endif
@ -116,14 +59,14 @@
</div> </div>
<div class="col-2"></div> <div class="col-2"></div>
</div> </div>
<div class="row d-none pt-3">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2">
<span id="form-reset" class="btn btn-outline-danger">@lang('Reset')</span>
<span id="form-submit" class="btn btn-success">@lang('Update')</span>
</div>
</div>
</form> </form>
<div class="row d-none pt-3">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2">
<x-form.reset form="dn-edit"/>
<x-form.submit action="Update" form="dn-edit"/>
</div>
</div>
</div> </div>
<!-- Internal Attributes --> <!-- Internal Attributes -->
@ -169,7 +112,7 @@
@endsection @endsection
@section('page-modals') @section('page-modals')
<!-- Modal --> <!-- EXPORT -->
<div class="modal fade" id="entry-export-modal" tabindex="-1" aria-labelledby="entry-export-label" aria-hidden="true"> <div class="modal fade" id="entry-export-modal" tabindex="-1" aria-labelledby="entry-export-label" aria-hidden="true">
<div class="modal-dialog modal-lg modal-fullscreen-xl-down"> <div class="modal-dialog modal-lg modal-fullscreen-xl-down">
<div class="modal-content"> <div class="modal-content">
@ -184,11 +127,47 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
<button id="entry-export-download" type="button" class="btn btn-primary btn-sm">Download</button> <button type="button" class="btn btn-primary btn-sm" id="entry-export-download">Download</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@if($up=$o->getObject('userpassword'))
<!-- CHECK USERPASSWORD -->
<div class="modal fade" id="userpassword-check-modal" tabindex="-1" aria-labelledby="userpassword-check-label" aria-hidden="true">
<div class="modal-dialog modal-lg modal-fullscreen-lg-down">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="userpassword-check-label">Check Passwords for {{ $dn }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table table-bordered p-1">
@foreach($up->values as $key => $value)
<tr>
<th>Check</th>
<td>{{ $up->render_item_old($key) }}</td>
<td>
<input type="password" style="width: 90%" name="password[{{$key}}]"> <i class="fas fa-fw fa-lock"></i>
<div class="invalid-feedback pb-2">
Invalid Password
</div>
</td>
</tr>
@endforeach
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-sm" id="userpassword_check_submit"><i class="fas fa-fw fa-spinner fa-spin d-none"></i> Check</button>
</div>
</div>
</div>
</div>
@endif
@endsection @endsection
@section('page-scripts') @section('page-scripts')
@ -213,6 +192,11 @@
$(this).attr('readonly',false); $(this).attr('readonly',false);
}); });
// Our password type
$('div#userpassword .form-select').each(function() {
$(this).prop('disabled',false);
})
$('.row.d-none').removeClass('d-none'); $('.row.d-none').removeClass('d-none');
$('.addable.d-none').removeClass('d-none'); $('.addable.d-none').removeClass('d-none');
$('.deletable.d-none').removeClass('d-none'); $('.deletable.d-none').removeClass('d-none');
@ -223,14 +207,6 @@
} }
$(document).ready(function() { $(document).ready(function() {
$('#form-reset').click(function() {
$('#dn-edit')[0].reset();
});
$('#form-submit').click(function() {
$('#dn-edit')[0].submit();
});
$('#newattr').on('change',function(item) { $('#newattr').on('change',function(item) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
@ -270,10 +246,9 @@
download('ldap-export.ldif',ldif.html()); download('ldap-export.ldif',ldif.html());
}); });
$('#entry-export-modal').on('shown.bs.modal', function () { $('#entry-export-modal').on('shown.bs.modal',function() {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
beforeSend: function() {},
success: function(data) { success: function(data) {
$('#entry-export').empty().append(data); $('#entry-export').empty().append(data);
}, },
@ -286,6 +261,65 @@
}) })
}) })
@if($up)
$('button[id=userpassword_check_submit]').on('click',function(item) {
var that = $(this);
var passwords = $('#userpassword-check-modal')
.find('input[name^="password["')
.map((key,item)=>item.value);
if (passwords.length === 0) return false;
$.ajax({
type: 'POST',
beforeSend: function() {
// Disable submit, add spinning icon
that.prop('disabled',true);
that.find('i').removeClass('d-none');
},
complete: function() {
that.prop('disabled',false);
that.find('i').addClass('d-none');
},
success: function(data) {
data.forEach(function(item,key) {
var i = $('#userpassword-check-modal')
.find('input[name="password['+key+']')
.siblings('i');
var feedback = $('#userpassword-check-modal')
.find('input[name="password['+key+']')
.siblings('div.invalid-feedback');
console.log(feedback.attr('display'));
if (item === 'OK') {
i.removeClass('text-danger').addClass('text-success').removeClass('fa-lock').addClass('fa-lock-open');
if (feedback.is(':visible'))
feedback.hide();
} else {
i.removeClass('text-success').addClass('text-danger').removeClass('fa-lock-open').addClass('fa-lock');
if (! feedback.is(':visible'))
feedback.show();
}
})
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/password/check') }}',
data: {
dn: '{{ $o->getDNSecure() }}',
password: Array.from(passwords),
},
dataType: 'json',
cache: false
})
});
@endif
@if(old()) @if(old())
editmode(); editmode();
@endif @endif

View File

@ -52,13 +52,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-footer">
<span class="ms-auto">
<button type="submit" class="btn btn-success btn-sm">Process</button>
</span>
</div>
</form> </form>
<div class="card-footer">
<span class="ms-auto">
<x-form.submit action="Process" form="import-form"/>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
--}} --}}
@section('main-content') @section('main-content')
<div class="card card-solid"> <div class="card card-solid mb-3">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-12 col-sm-4"> <div class="col-12 col-sm-4">
@ -38,19 +38,7 @@
</div> </div>
</div> </div>
@if(file_exists('home-note.html')) <x-file-note file="home-note.html"/>
<hr>
<div class="row">
<div class="col-12 offset-lg-2 col-lg-8">
<div class="mx-auto card text-white card-body bg-primary">
<h5 class="text-white card-title"><i class="icon fa-2x fas fa-info pe-3"></i><span class="font-size-xlg">NOTE</span></h5>
<span class="w-100 pb-0">
{!! file_get_contents('home-note.html') !!}
</span>
</div>
</div>
</div>
@endif
@endsection @endsection
@section('page-scripts') @section('page-scripts')

View File

@ -11,4 +11,5 @@
</div> </div>
@yield('page-modals') @yield('page-modals')
@yield('page-scripts') @yield('page-scripts')
@yield('page-styles')

View File

@ -1,83 +1,25 @@
@extends('home') @extends('home')
@section('page_title') @section('page_title')
<table class="table table-borderless"> @include('fragment.dn.header')
<tr>
<td class="{{ ($x=$o->getObject('jpegphoto')) ? 'border' : '' }}" rowspan="2">
{!! $x ? $x->render(FALSE,TRUE) : 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>
</tr>
<tr>
<td class="line-height-1" style="font-size: 55%;vertical-align: bottom;" colspan="2">
<table>
<tr>
<td class="p-1 m-1">Created</td>
<th class="p-1 m-1">
{{ ($x=$o->getObject('createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('creatorsname')) ? $x->render() : __('Unknown') }}]
</th>
</tr>
<tr>
<td class="p-1 m-1">Modified</td>
<th class="p-1 m-1">
{{ ($x=$o->getObject('modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('modifiersname')) ? $x->render() : __('Unknown') }}]
</th>
</tr>
<tr>
<td class="p-1 m-1">UUID</td>
<th class="p-1 m-1">{{ $o->entryuuid[0] ?? '' }}</th>
</tr>
</table>
</td>
</tr>
</table>
@endsection @endsection
@section('main-content') @section('main-content')
@if(session()->has('note')) <x-note/>
<div class="alert alert-info"> <x-success/>
<h4 class="alert-heading"><i class="fas fa-fw fa-note-sticky"></i> Note:</h4> <x-error/>
<hr>
<p>{{ session()->pull('note') }}</p>
</div>
@endif
@if(session()->has('success'))
<div class="alert alert-success">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
<hr>
<p>{{ session()->pull('success') }}</p>
<ul style="list-style-type: square;">
@foreach (session()->pull('updated') as $key => $values)
<li>{{ $key }}: {{ join(',',$values) }}</li>
@endforeach
</ul>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-down"></i> Error?</h4>
<hr>
<ul style="list-style-type: square;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="main-card mb-3 card"> <div class="main-card mb-3 card">
<form id="dn-update" method="POST" class="needs-validation" action="{{ url('entry/update/commit') }}" novalidate> <form id="dn-update" method="POST" class="needs-validation" action="{{ url('entry/update/commit') }}" novalidate>
@csrf @csrf
<input type="hidden" name="dn" value="{{ $o->getDNSecure() }}"> <input type="hidden" name="dn" value="{{ $o->getDNSecure() }}">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 col-xl-4 mx-auto pt-3"> <div class="col-12 col-lg-6 mx-auto pt-3">
<div class="card-title"><h3>@lang('Do you want to make the following changes?')</h3></div> <div class="card-title"><h3>@lang('Do you want to make the following changes?')</h3></div>
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped w-100">
<thead> <thead>
<tr> <tr>
<th>Attribute</th> <th>Attribute</th>
@ -87,16 +29,18 @@
</thead> </thead>
<tbody> <tbody>
@foreach ($o->getDirty() as $key => $value) @foreach ($o->getAttributesAsObjects()->filter(fn($item)=>$item->isDirty()) as $key => $oo)
<tr> <tr>
<th rowspan="{{ $x=max(count($value),count(Arr::get($o->getOriginal(),$key,[])))}}">{{ $key }}</th> <th rowspan="{{ $x=max($oo->values->keys()->max(),$oo->old_values->keys()->max())+1}}">
<abbr title="{{ $oo->description }}">{{ $oo->name }}</abbr>
</th>
@for($xx=0;$xx<$x;$xx++) @for($xx=0;$xx<$x;$xx++)
@if($xx) @if($xx)
</tr><tr> </tr><tr>
@endif @endif
<td>{{ Arr::get(Arr::get($o->getOriginal(),$key),$xx,'['.strtoupper(__('New Value')).']') }}</td> <td>{{ $oo->render_item_old($xx) ?: '['.strtoupper(__('New Value')).']' }}</td>
<td>{{ ($y=Arr::get($value,$xx)) ?: '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[]" value="{{ $y }}"></td> <td>{{ $oo->render_item_new($xx) ?: '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[]" value="{{ Arr::get($oo->values,$xx) }}"></td>
@endfor @endfor
</tr> </tr>
@endforeach @endforeach
@ -104,28 +48,15 @@
</table> </table>
</div> </div>
</div> </div>
<div class="row pt-3">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2 mx-auto">
<span id="form-reset" class="btn btn-outline-danger">@lang('Reset')</span>
<span id="form-submit" class="btn btn-success">@lang('Update')</span>
</div>
</div>
</div> </div>
</form> </form>
<div class="row p-3">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2 mx-auto">
<x-form.cancel/>
<x-form.submit action="Update" form="dn-update"/>
</div>
</div>
</div> </div>
@endsection @endsection
@section('page-scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#form-reset').click(function() {
$('#dn-update')[0].reset();
});
$('#form-submit').click(function() {
$('#dn-update')[0].submit();
});
});
</script>
@append

View File

@ -42,5 +42,6 @@ Route::post('entry/update/commit',[HomeController::class,'entry_update']);
Route::post('entry/update/pending',[HomeController::class,'entry_pending_update']); Route::post('entry/update/pending',[HomeController::class,'entry_pending_update']);
Route::get('entry/newattr/{id}',[HomeController::class,'entry_newattr']); Route::get('entry/newattr/{id}',[HomeController::class,'entry_newattr']);
Route::get('entry/export/{id}',[HomeController::class,'entry_export']); Route::get('entry/export/{id}',[HomeController::class,'entry_export']);
Route::post('entry/password/check/',[HomeController::class,'entry_password_check']);
Route::post('import/process/{type}',[HomeController::class,'import']); Route::post('import/process/{type}',[HomeController::class,'import']);