From 6b279d1df1d9356692db5d9f18c5ea7786fa5e59 Mon Sep 17 00:00:00 2001 From: Deon George Date: Thu, 3 Jul 2025 20:42:14 +0800 Subject: [PATCH] Implement Entry Copy/Move --- app/Classes/LDAP/Server.php | 11 ++ app/Http/Controllers/AjaxController.php | 23 +++ app/Http/Controllers/HomeController.php | 60 +++++- app/Ldap/Entry.php | 5 +- public/css/custom.css | 7 +- resources/views/frames/dn.blade.php | 23 ++- .../views/modals/entry-copy-move.blade.php | 178 ++++++++++++++++++ routes/web.php | 6 +- 8 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 resources/views/modals/entry-copy-move.blade.php diff --git a/app/Classes/LDAP/Server.php b/app/Classes/LDAP/Server.php index 5f2f5576..d3a51792 100644 --- a/app/Classes/LDAP/Server.php +++ b/app/Classes/LDAP/Server.php @@ -509,4 +509,15 @@ final class Server { return Arr::get($this->rootDSE->subschemasubentry,0); } + + public function subordinates(string $dn,array $attrs=['dn']): ?LDAPCollection + { + return $this + ->get( + dn: $dn, + attrs: array_merge($attrs,[])) + ->rawFilter('(hassubordinates=TRUE)') + ->search() + ->get() ?: NULL; + } } \ No newline at end of file diff --git a/app/Http/Controllers/AjaxController.php b/app/Http/Controllers/AjaxController.php index 8bc26a34..aaa7b9cf 100644 --- a/app/Http/Controllers/AjaxController.php +++ b/app/Http/Controllers/AjaxController.php @@ -113,4 +113,27 @@ class AjaxController extends Controller 'may' => $oc->getMayAttrs()->pluck('name'), ]; } + + public function subordinates(?string $dn=NULL): array + { + $dn = $dn ? Crypt::decryptString($dn) : ''; + + // Sometimes our key has a command, so we'll ignore it + if (str_starts_with($dn,'*') && ($x=strpos($dn,'|'))) + $dn = substr($dn,$x+1); + + $result = collect(); + // If no DN, we'll find all children + if (! $dn) + foreach (Server::baseDNs() as $base) + $result = $result->merge(config('server') + ->subordinates($base->getDN())); + else + $result = config('server') + ->subordinates(collect(explode(',',$dn))->last()); + + return + $result->map(fn($item)=>['id'=>$item->getDNSecure(),'value'=>$item->getDN()]) + ->toArray(); + } } \ No newline at end of file diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 7b00f529..36796ed8 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -112,6 +112,60 @@ class HomeController extends Controller ->with('updated',FALSE); } + public function entry_copy_move(Request $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View + { + $from_dn = Crypt::decryptString($request->post('dn')); + Log::info(sprintf('%s:Renaming [%s] to [%s]',self::LOGKEY,$from_dn,$request->post('to_dn'))); + + $o = clone config('server')->fetch($from_dn); + + if (! $o) + return back() + ->withInput() + ->with('note',__('DN doesnt exist')); + + $o->setDN($request->post('to_dn')); + $o->exists = FALSE; + + // Add the RDN attribute to match the new RDN + $rdn = collect(explode(',',$request->post('to_dn')))->first(); + + list($attr,$value) = explode('=',$rdn); + $o->{$attr} = [Entry::TAG_NOTAG => $o->getObject($attr)->tagValuesOld(Entry::TAG_NOTAG)->push($value)->unique()]; + + Log::info(sprintf('%s:Copying [%s] to [%s]',self::LOGKEY,$from_dn,$o->getDN())); + + try { + $o->save(); + + } catch (LdapRecordException $e) { + return Redirect::to('/') + ->withInput(['_key'=>Crypt::encryptString('*dn|'.$from_dn)]) + ->with('failed',sprintf('%s: %s - %s: %s', + __('LDAP Server Error Code'), + $e->getDetailedError()?->getErrorCode() ?: $e->getMessage(), + $e->getDetailedError()?->getErrorMessage() ?: $e->getFile(), + $e->getDetailedError()?->getDiagnosticMessage() ?: $e->getLine(), + )); + } + + if ($request->post('delete') && $request->post('delete') === '1') { + Log::info(sprintf('%s:Deleting [%s] after copy',self::LOGKEY,$from_dn)); + + $x = $this->entry_delete($request); + + return ($x->getSession()->has('success')) + ? Redirect::to('/') + ->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())]) + ->with('success',__('Entry copied and deleted')) + : $x; + } + + return Redirect::to('/') + ->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())]) + ->with('success',__('Entry copied')); + } + public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse { $key = $this->request_key($request,collect(old())); @@ -154,7 +208,7 @@ class HomeController extends Controller ->with('failed',sprintf('%s: %s - %s: %s', __('LDAP Server Error Code'), $e->getDetailedError()->getErrorCode(), - __($e->getDetailedError()->getErrorMessage()), + $e->getDetailedError()->getErrorMessage(), $e->getDetailedError()->getDiagnosticMessage(), )); } @@ -377,7 +431,7 @@ class HomeController extends Controller ->with('failed',sprintf('%s: %s - %s: %s', __('LDAP Server Error Code'), $e->getDetailedError()->getErrorCode(), - __($e->getDetailedError()->getErrorMessage()), + $e->getDetailedError()->getErrorMessage(), $e->getDetailedError()->getDiagnosticMessage(), )); } @@ -435,8 +489,8 @@ class HomeController extends Controller ->with('dn',$key['dn']) ->with('o',$o) ->with('page_actions',collect([ - 'copy'=>FALSE, 'create'=>($x=($o->getObjects()->except('entryuuid')->count() > 0)), + 'copy'=>$x, 'delete'=>(! is_null($xx=$o->getObject('hassubordinates')->value)) && ($xx === 'FALSE'), 'edit'=>$x, 'export'=>$x, diff --git a/app/Ldap/Entry.php b/app/Ldap/Entry.php index 5923a91a..d266d549 100644 --- a/app/Ldap/Entry.php +++ b/app/Ldap/Entry.php @@ -92,11 +92,12 @@ class Entry extends Model public function getAttributes(): array { return $this->objects + ->filter(fn($item)=>(! $item->is_internal)) ->flatMap(fn($item)=> - ($item->no_attr_tags) + $item->no_attr_tags ? [strtolower($item->name)=>$item->values] : $item->values - ->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v])) + ->flatMap(fn($v,$k)=>[strtolower($item->name.(($k !== self::TAG_NOTAG) ? ';'.$k : ''))=>$v])) ->toArray(); } diff --git a/public/css/custom.css b/public/css/custom.css index fe843a7b..a8f46350 100644 --- a/public/css/custom.css +++ b/public/css/custom.css @@ -19,7 +19,7 @@ attribute#objectclass .input-group-end:not(input.form-control) { border-radius: 4px !important; } -.input-group:first-child:not(.select-group) .select2-container--bootstrap-5 .select2-selection { +.input-group:first-child:not(.select-group):not(:last-child) .select2-container--bootstrap-5 .select2-selection { border-bottom-right-radius: unset; border-top-right-radius: unset; } @@ -97,4 +97,9 @@ input.form-control.input-group-end { /* Square UL items */ ul.square { list-style-type: square; +} + +/* Fix select2 selections when set up with d-none */ +select[class*="d-none"] + span.select2 { + display: none; } \ No newline at end of file diff --git a/resources/views/frames/dn.blade.php b/resources/views/frames/dn.blade.php index 0cd0e5e8..36d9ea3c 100644 --- a/resources/views/frames/dn.blade.php +++ b/resources/views/frames/dn.blade.php @@ -23,7 +23,9 @@ @endif @if($page_actions->get('copy'))
  • - + + +
  • @endif @if($page_actions->get('edit')) @@ -226,6 +228,25 @@ var that = $(this).find('.modal-content'); switch ($(item.relatedTarget).attr('id')) { + case 'entry-copy-move': + $.ajax({ + method: 'GET', + url: '{{ url('modal/copy-move') }}/'+dn, + dataType: 'html', + cache: false, + beforeSend: function() { + that.empty().append(''); + }, + success: function(data) { + that.empty().html(data); + }, + error: function(e) { + if (e.status !== 412) + alert('That didnt work? Please try again....'); + }, + }); + break; + case 'entry-delete': $.ajax({ method: 'GET', diff --git a/resources/views/modals/entry-copy-move.blade.php b/resources/views/modals/entry-copy-move.blade.php new file mode 100644 index 00000000..776f25c9 --- /dev/null +++ b/resources/views/modals/entry-copy-move.blade.php @@ -0,0 +1,178 @@ + + + + + + + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index d2d8be17..f98c8204 100644 --- a/routes/web.php +++ b/routes/web.php @@ -43,18 +43,21 @@ Route::controller(HomeController::class)->group(function() { }); Route::match(['get','post'],'entry/add','entry_add'); + Route::post('entry/attr/add/{id}','entry_attr_add'); Route::post('entry/create','entry_create'); + Route::post('entry/copy-move','entry_copy_move'); Route::post('entry/delete','entry_delete'); Route::get('entry/export/{id}','entry_export'); Route::post('entry/password/check/','entry_password_check'); - Route::post('entry/attr/add/{id}','entry_attr_add'); Route::post('entry/objectclass/add','entry_objectclass_add'); Route::post('entry/rename','entry_rename'); + Route::post('entry/update/commit','entry_update'); Route::post('entry/update/pending','entry_pending_update'); Route::post('import/process/{type}','import'); + Route::view('modal/copy-move/{dn}','modals.entry-copy-move'); Route::view('modal/delete/{dn}','modals.entry-delete'); Route::view('modal/export/{dn}','modals.entry-export'); Route::view('modal/rename/{dn}','modals.entry-rename'); @@ -69,4 +72,5 @@ Route::controller(AjaxController::class) Route::post('children','children'); Route::post('schema/view','schema_view'); Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs'); + Route::post('subordinates','subordinates'); }); \ No newline at end of file