diff --git a/app/Classes/LDAP/Attribute.php b/app/Classes/LDAP/Attribute.php index 5a185bc..e636a0c 100644 --- a/app/Classes/LDAP/Attribute.php +++ b/app/Classes/LDAP/Attribute.php @@ -168,6 +168,11 @@ class Attribute implements \Countable, \ArrayAccess return $this->name; } + public function addValue(string $value): void + { + $this->values->push($value); + } + public function count(): int { return $this->values->count(); diff --git a/app/Classes/LDAP/Attribute/Factory.php b/app/Classes/LDAP/Attribute/Factory.php index a45b8ea..08856a6 100644 --- a/app/Classes/LDAP/Attribute/Factory.php +++ b/app/Classes/LDAP/Attribute/Factory.php @@ -50,7 +50,7 @@ class Factory */ public static function create(string $attribute,array $values): Attribute { - $class = Arr::get(self::map,$attribute,Attribute::class); + $class = Arr::get(self::map,strtolower($attribute),Attribute::class); Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class)); return new $class($attribute,$values); diff --git a/app/Classes/LDAP/Export.php b/app/Classes/LDAP/Export.php new file mode 100644 index 0000000..1abd1b9 --- /dev/null +++ b/app/Classes/LDAP/Export.php @@ -0,0 +1,53 @@ +items = $items; + } + + abstract public function __toString(): string; + + protected function header() + { + $output = ''; + + $output .= sprintf('# %s %s',__(static::type.' for'),($x=$this->items->first())).$this->br; + $output .= sprintf('# %s: %s (%s)', + __('Server'), + $x->getConnection()->getConfiguration()->get('name'), + $x->getConnection()->getLdapConnection()->getHost()).$this->br; + //$output .= sprintf('# %s: %s',__('Search Scope'),$this->scope).$this->br; + //$output .= sprintf('# %s: %s',__('Search Filter'),$this->entry->dn).$this->br; + $output .= sprintf('# %s: %s',__('Total Entries'),$this->items->count()).$this->br; + $output .= '#'.$this->br; + $output .= sprintf('# %s %s (%s) on %s',__('Generated by'),config('app.name'),config('app.url'),date('F j, Y g:i a')).$this->br; + $output .= sprintf('# %s %s',__('Exported by'),Auth::user() ?: 'Anonymous').$this->br; + $output .= sprintf('# %s: %s',__('Version'),config('app.version')).$this->br; + + $output .= $this->br; + + return $output; + } +} \ No newline at end of file diff --git a/app/Classes/LDAP/Export/LDIF.php b/app/Classes/LDAP/Export/LDIF.php new file mode 100644 index 0000000..873f37f --- /dev/null +++ b/app/Classes/LDAP/Export/LDIF.php @@ -0,0 +1,78 @@ +br; + + $c = 1; + foreach ($this->items as $o) { + if ($c > 1) + $result .= $this->br; + + $title = (string)$o; + if (strlen($title) > $this->line_length) + $title = Str::of($title)->limit($this->line_length-3-5,'...'.substr($title,-5)); + + $result .= sprintf('# %s %s: %s',__('Entry'),$c++,$title).$this->br; + + // Display DN + $result .= $this->multiLineDisplay( + Str::isAscii($o) + ? sprintf('dn: %s',$o) + : sprintf('dn:: %s',base64_encode($o)) + ,$this->br); + + // Display Attributes + foreach ($o->getObjects() as $ao) { + foreach ($ao->values as $value) { + $result .= $this->multiLineDisplay( + Str::isAscii($value) + ? sprintf('%s: %s',$ao->name,$value) + : sprintf('%s:: %s',$ao->name,base64_encode($value)) + ,$this->br); + } + } + } + + return $result; + } + + /** + * Helper method to wrap LDIF lines + * + * @param string $str The line to be wrapped if needed. + */ + private function multiLineDisplay(string $str,string $br): string + { + $length_string = strlen($str); + $length_max = $this->line_length; + + $output = ''; + while ($length_string > $length_max) { + $output .= substr($str,0,$length_max).$br; + $str = ' '.substr($str,$length_max); + $length_string = strlen($str); + } + + $output .= $str.$br; + + return $output; + } +} \ No newline at end of file diff --git a/app/Classes/LDAP/Import.php b/app/Classes/LDAP/Import.php new file mode 100644 index 0000000..3340c18 --- /dev/null +++ b/app/Classes/LDAP/Import.php @@ -0,0 +1,79 @@ + self::LDAP_IMPORT_ADD, + 'delete' => self::LDAP_IMPORT_DELETE, + 'modrdn' => self::LDAP_IMPORT_MODRDN, + 'moddn' => self::LDAP_IMPORT_MODDN, + 'modify' => self::LDAP_IMPORT_MODIFY, + ]; + + // The import data to process + protected string $input; + // The attributes the server knows about + protected Collection $server_attributes; + + public function __construct(string $input) { + $this->input = $input; + $this->server_attributes = config('server')->schema('attributetypes'); + } + + /** + * Attempt to commit an entry and return the result. + * + * @param Entry $o + * @param int $action + * @return Collection + * @throws GeneralException + * @throws ObjectExistsException + */ + final protected function commit(Entry $o,int $action): Collection + { + switch ($action) { + case static::LDAP_IMPORT_ADD: + try { + $o->save(); + + } catch (\Exception $e) { + return collect([ + 'dn'=>$o->getDN(), + 'result'=>sprintf('%d: %s (%s)', + ($x=$e->getDetailedError())->getErrorCode(), + $x->getErrorMessage(), + $x->getDiagnosticMessage(), + ) + ]); + } + + return collect(['dn'=>$o->getDN(),'result'=>__('Created')]); + + default: + throw new GeneralException('Unhandled action during commit: '.$action); + } + } + + abstract public function process(): Collection; +} \ No newline at end of file diff --git a/app/Classes/LDAP/Import/LDIF.php b/app/Classes/LDAP/Import/LDIF.php new file mode 100644 index 0000000..9626189 --- /dev/null +++ b/app/Classes/LDAP/Import/LDIF.php @@ -0,0 +1,233 @@ +input) as $line) { + $c++; + Log::debug(sprintf('%s: LDIF Line [%s]',self::LOGKEY,$line)); + $line = trim($line); + + // If the line starts with a comment, ignore it + if (preg_match('/^#/',$line)) + continue; + + // If we have a blank line, then that completes this command + if (! $line) { + if (! is_null($o)) { + // Add the last attribute; + $o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); + + Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN())); + + // Commit + $result->push($this->commit($o,$action)); + $result->last()->put('line',$c); + + $o = NULL; + $action = NULL; + $base64encoded = FALSE; + $attribute = NULL; + $value = ''; + + // Else its a blank line + } + + continue; + } + + $m = []; + preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m); + + switch ($x=Arr::get($m,1)) { + case 'changetype': + if ($m[2] !== ':') + throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c)); + + switch ($m[3]) { + // if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) { + default: + throw new NotImplementedException(sprintf('Unknown change type [%s]? (line %d)',$m[3],$c)); + } + + break; + + case 'version': + if (! is_null($version)) + throw new VersionException(sprintf('Version has already been set at [%d]. (line %d)',$version,$c)); + + if ($m[2] !== ':') + throw new VersionException(sprintf('Version cannot be base64 encoded set at [%d]. (line %d)',$version,$c)); + + $version = (int)$m[3]; + break; + + // Treat it as an attribute + default: + // If $m is NULL, then this is the 2nd (or more) line of a base64 encoded value + if (! $m) { + $value .= $line; + Log::debug(sprintf('%s: Attribute [%s] adding [%s] (%d)',self::LOGKEY,$attribute,$line,$c)); + + // add to last attr value + continue 2; + } + + // We are ready to create the entry or add the attribute + if ($attribute) { + if ($attribute === 'dn') { + if (! is_null($o)) + throw new GeneralException(sprintf('Previous Entry not complete? (line %d)',$c)); + + $dn = $base64encoded ? base64_decode($value) : $value; + Log::debug(sprintf('%s: Creating new entry:',self::LOGKEY,$dn)); + //$o = Entry::find($dn); + + // If it doesnt exist, we'll create it + //if (! $o) { + $o = new Entry; + $o->setDn($dn); + //} + + $action = self::LDAP_IMPORT_ADD; + + } else { + Log::debug(sprintf('%s: Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c)); + + if ($value) + $o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); + else + throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c)); + } + } + + // Start of a new attribute + $base64encoded = ($m[2] === '::'); + // @todo Need to parse attributes with ';' options + $attribute = $m[1]; + $value = $m[3]; + + Log::debug(sprintf('%s: New Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c)); + } + + if ($version !== 1) + throw new VersionException('LDIF import cannot handle version: '.($version ?: __('NOT DEFINED'))); + } + + // We may still have a pending action + if ($action) { + // Add the last attribute; + $o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); + + Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN())); + + // Commit + $result->push($this->commit($o,$action)); + $result->last()->put('line',$c); + } + + return $result; + } + + public function readEntry() { + static $haveVersion = false; + + if ($lines = $this->nextLines()) { + + $server = $this->getServer(); + + # The first line should be the DN + if (preg_match('/^dn:/',$lines[0])) { + list($text,$dn) = $this->getAttrValue(array_shift($lines)); + + # The second line should be our changetype + if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) { + $attrvalue = $this->getAttrValue($lines[0]); + $changetype = $attrvalue[1]; + array_shift($lines); + + } else + $changetype = 'add'; + + $this->template = new Template($this->server_id,null,null,$changetype); + + switch ($changetype) { + case 'add': + $rdn = get_rdn($dn); + $container = $server->getContainer($dn); + + $this->template->setContainer($container); + $this->template->accept(); + + $this->getAddDetails($lines); + $this->template->setRDNAttributes($rdn); + + return $this->template; + + break; + + case 'modify': + if (! $server->dnExists($dn)) + return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines); + + $this->template->setDN($dn); + $this->template->accept(false,true); + + return $this->getModifyDetails($lines); + + break; + + case 'moddn': + case 'modrdn': + if (! $server->dnExists($dn)) + return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines); + + $this->template->setDN($dn); + $this->template->accept(); + + return $this->getModRDNAttributes($lines); + + break; + + default: + if (! $server->dnExists($dn)) + return $this->error(_('Unkown change type'),$lines); + } + + } else + return $this->error(_('A valid dn line is required'),$lines); + + } else + return false; + } +} \ No newline at end of file diff --git a/app/Exceptions/Import/AttributeException.php b/app/Exceptions/Import/AttributeException.php new file mode 100644 index 0000000..eab14ce --- /dev/null +++ b/app/Exceptions/Import/AttributeException.php @@ -0,0 +1,7 @@ +transform(function($item) { + return [ + 'title'=>$item->getRdn(), + 'item'=>$item->getDNSecure(), + 'lazy'=>TRUE, + 'icon'=>'fa-fw fas fa-sitemap', + 'tooltip'=>$item->getDn(), + ]; + }); + } + /** * Debug Page * @@ -49,6 +68,22 @@ class HomeController extends Controller ->with('page_actions',$page_actions); } + public function entry_export(Request $request,string $id) + { + $dn = Crypt::decryptString($id); + + $result = (new Entry) + ->query() + //->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) + //->select(['*']) + ->setDn($dn) + ->recursive() + ->get(); + + return view('fragment.export') + ->with('result',new LDIFExport($result)); + } + public function entry_newattr(string $id) { $x = new AttributeType(new Attribute($id,[]),TRUE); @@ -76,20 +111,8 @@ class HomeController extends Controller ->withInput() ->with('note',__('No attributes changed')); - $base = Server::baseDNs() ?: collect(); - - $bases = $base->transform(function($item) { - return [ - 'title'=>$item->getRdn(), - 'item'=>$item->getDNSecure(), - 'lazy'=>TRUE, - 'icon'=>'fa-fw fas fa-sitemap', - 'tooltip'=>$item->getDn(), - ]; - }); - - return view('frames.update') - ->with('bases',$bases) + return view('update') + ->with('bases',$this->bases()) ->with('dn',$dn) ->with('o',$o); } @@ -103,18 +126,6 @@ class HomeController extends Controller */ public function entry_update(EntryRequest $request) { - $base = Server::baseDNs() ?: collect(); - - $bases = $base->transform(function($item) { - return [ - 'title'=>$item->getRdn(), - 'item'=>$item->getDNSecure(), - 'lazy'=>TRUE, - 'icon'=>'fa-fw fas fa-sitemap', - 'tooltip'=>$item->getDn(), - ]; - }); - $dn = Crypt::decryptString($request->dn); $o = config('server')->fetch($dn); @@ -168,51 +179,75 @@ class HomeController extends Controller */ public function home() { - $base = Server::baseDNs() ?: collect(); - - $bases = $base->transform(function($item) { - return [ - 'title'=>$item->getRdn(), - 'item'=>$item->getDNSecure(), - 'lazy'=>TRUE, - 'icon'=>'fa-fw fas fa-sitemap', - 'tooltip'=>$item->getDn(), - ]; - }); - if (old('dn')) return view('frame') ->with('subframe','dn') - ->with('bases',$bases) + ->with('bases',$this->bases()) ->with('o',config('server')->fetch($dn=Crypt::decryptString(old('dn')))) ->with('dn',$dn); elseif (old('frame')) return view('frame') ->with('subframe',old('frame')) - ->with('bases',$bases); + ->with('bases',$this->bases()); else return view('home') - ->with('bases',$bases) + ->with('bases',$this->bases()) ->with('server',config('ldap.connections.default.name')); } + /** + * Process the incoming LDIF file or LDIF text + * + * @param ImportRequest $request + * @param string $type + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application + * @throws GeneralException + * @throws VersionException + */ + public function import(ImportRequest $request,string $type) + { + switch ($type) { + case 'ldif': + $import = new LDIFImport($x=($request->text ?: $request->file->get())); + break; + + default: + abort(404,'Unknown import type: '.$type); + } + + try { + $result = $import->process(); + + } catch (NotImplementedException $e) { + abort(555,$e->getMessage()); + + } catch (\Exception $e) { + abort(598,$e->getMessage()); + } + + return view('frame') + ->with('subframe','import_result') + ->with('bases',$this->bases()) + ->with('result',$result) + ->with('ldif',htmlspecialchars($x)); + } + + public function import_frame() + { + return view('frames.import'); + } + /** * LDAP Server INFO * * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View - * @throws ObjectNotFoundException */ public function info() { - // Load our attributes - $s = config('server'); - $s->schema('objectclasses'); - $s->schema('attributetypes'); - return view('frames.info') - ->with('s',$s); + ->with('s',config('server')); } /** diff --git a/app/Http/Requests/ImportRequest.php b/app/Http/Requests/ImportRequest.php new file mode 100644 index 0000000..abdf8c1 --- /dev/null +++ b/app/Http/Requests/ImportRequest.php @@ -0,0 +1,22 @@ + 'required|string|in:import', + 'file' => 'nullable|extensions:ldif|required_without:text', + 'text'=> 'nullable|prohibits:file|string|min:16', + ]; + } +} \ No newline at end of file diff --git a/app/Ldap/Entry.php b/app/Ldap/Entry.php index 9fc0726..edc1e18 100644 --- a/app/Ldap/Entry.php +++ b/app/Ldap/Entry.php @@ -2,21 +2,50 @@ namespace App\Ldap; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Crypt; +use LdapRecord\Support\Arr; use LdapRecord\Models\Model; +use LdapRecord\Query\Model\Builder; use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute\Factory; +use App\Classes\LDAP\Export\LDIF; +use App\Exceptions\Import\AttributeException; class Entry extends Model { + private Collection $objects; + private bool $noObjectAttributes = FALSE; + /* OVERRIDES */ + public function __construct(array $attributes = []) + { + $this->objects = collect(); + + parent::__construct($attributes); + } + + public function discardChanges(): static + { + parent::discardChanges(); + + // If we are discharging changes, we need to reset our $objects; + $this->objects = $this->getAttributesAsObjects($this->attributes); + + return $this; + } + + /** + * This function overrides getAttributes to use our collection of Attribute objects instead of the models attributes. + * + * @return array + * @note $this->attributes may not be updated with changes + */ public function getAttributes(): array { - return $this->getAttributesAsObjects()->toArray(); + return $this->objects->map(function($item) { return $item->values->toArray(); })->toArray(); } /** @@ -24,57 +53,80 @@ class Entry extends Model */ protected function originalIsEquivalent(string $key): bool { - if (! array_key_exists($key, $this->original)) { - return false; + $key = $this->normalizeAttributeKey($key); + + if ((! array_key_exists($key, $this->original)) && (! $this->objects->has($key))) { + return TRUE; } $current = $this->attributes[$key]; - $original = $this->original[$key]; + $original = $this->objects->get($key)->values; if ($current === $original) { return true; } - //dump(['key'=>$key,'current'=>$current,'original'=>$this->original[$key],'objectvalue'=>$this->getAttributeAsObject($key)->isDirty()]); - return ! $this->getAttributeAsObject($key)->isDirty(); + return ! $this->getObject($key)->isDirty(); } - public function getOriginal(): array + public static function query(bool $noattrs=false): Builder { - static $result = NULL; + $o = new static; - if (is_null($result)) { - $result = collect(); + if ($noattrs) + $o->noObjectAttributes(); - // @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]; + return $o->newQuery(); + } - // If the attribute doesnt exist we'll create it - $o = Arr::get($result,$attribute,Factory::create($attribute,[])); - $o->setLangTag($matches[3],$value); + /** + * As attribute values are updated, or new ones created, we need to mirror that + * into our $objects + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function setAttribute(string $key, mixed $value): static + { + parent::setAttribute($key,$value); - } else { - $o = Factory::create($attribute,$value); - } + $key = $this->normalizeAttributeKey($key); - if (! $result->has($attribute)) { - // Set the rdn flag - if (preg_match('/^'.$attribute.'=/i',$this->dn)) - $o->setRDN(); + if ((! $this->objects->get($key)) && $value) { + $o = new Attribute($key,[]); + $o->value = $value; - // Set required flag - $o->required_by(collect($this->getAttribute('objectclass'))); + $this->objects->put($key,$o); - $result->put($attribute,$o); - } - } + } elseif ($this->objects->get($key)) { + $this->objects->get($key)->value = $this->attributes[$key]; } - return $result->toArray(); + return $this; + } + + /** + * We'll shadow $this->attributes to $this->objects - a collection of Attribute objects + * + * Using the objects, it'll make it easier to work with attribute values + * + * @param array $attributes + * @return $this + */ + public function setRawAttributes(array $attributes = []): static + { + parent::setRawAttributes($attributes); + + // 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',[])))) { + $this->objects = $this->getAttributesAsObjects($this->attributes); + + } else { + $this->objects = collect(); + } + + return $this; } /* ATTRIBUTES */ @@ -92,88 +144,89 @@ class Entry extends Model /* METHODS */ - /** - * Get an attribute as an object - * - * @param string $key - * @return Attribute|null - */ - public function getAttributeAsObject(string $key): Attribute|null + public function addAttribute(string $key,mixed $value): void { - return Arr::get($this->getAttributesAsObjects(),$key); + $key = $this->normalizeAttributeKey($key); + + if (config('server')->schema('attributetypes')->has($key) === FALSE) + throw new AttributeException('Schema doesnt have attribute [%s]',$key); + + if ($x=$this->objects->get($key)) { + $x->addValue($value); + + } else { + $this->objects->put($key,Attribute\Factory::create($key,Arr::wrap($value))); + } } /** * Convert all our attribute values into an array of Objects * + * @param array $attributes * @return Collection */ - protected function getAttributesAsObjects(): Collection + protected function getAttributesAsObjects(array $attributes): Collection { - static $result = NULL; + $result = collect(); - if (is_null($result)) { - $result = collect(); + foreach ($attributes 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]; - foreach (parent::getAttributes() 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); - // 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'))); - - // Store our original value to know if this attribute has changed - if ($x=Arr::get($this->original,$attribute)) - $o->oldValues($x); - - $result->put($attribute,$o); - } + } else { + $o = Factory::create($attribute,$value); } - $sort = collect(config('ldap.attr_display_order',[]))->transform(function($item) { return strtolower($item); }); + if (! $result->has($attribute)) { + // Set the rdn flag + if (preg_match('/^'.$attribute.'=/i',$this->dn)) + $o->setRDN(); - // Order the attributes - $result = $result->sortBy([function(Attribute $a,Attribute $b) use ($sort): int { - if ($a === $b) - return 0; + // Set required flag + $o->required_by(collect($this->getAttribute('objectclass'))); - // Check if $a/$b are in the configuration to be sorted first, if so get it's key - $a_key = $sort->search($a->name_lc); - $b_key = $sort->search($b->name_lc); + // Store our original value to know if this attribute has changed + if ($x=Arr::get($this->original,$attribute)) + $o->oldValues($x); - // If the keys were not in the sort list, set the key to be the count of elements (ie: so it is last to be sorted) - if ($a_key === FALSE) - $a_key = $sort->count()+1; - - if ($b_key === FALSE) - $b_key = $sort->count()+1; - - // Case where neither $a, nor $b are in ldap.attr_display_order, $a_key = $b_key = one greater than num elements. - // So we sort them alphabetically - if ($a_key === $b_key) - return strcasecmp($a->name,$b->name); - - // 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 ($a_key < $b_key) ? -1 : 1; - } ]); + $result->put($attribute,$o); + } } + $sort = collect(config('ldap.attr_display_order',[]))->transform(function($item) { return strtolower($item); }); + + // Order the attributes + $result = $result->sortBy([function(Attribute $a,Attribute $b) use ($sort): int { + if ($a === $b) + return 0; + + // Check if $a/$b are in the configuration to be sorted first, if so get it's key + $a_key = $sort->search($a->name_lc); + $b_key = $sort->search($b->name_lc); + + // If the keys were not in the sort list, set the key to be the count of elements (ie: so it is last to be sorted) + if ($a_key === FALSE) + $a_key = $sort->count()+1; + + if ($b_key === FALSE) + $b_key = $sort->count()+1; + + // Case where neither $a, nor $b are in ldap.attr_display_order, $a_key = $b_key = one greater than num elements. + // So we sort them alphabetically + if ($a_key === $b_key) + return strcasecmp($a->name,$b->name); + + // 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 ($a_key < $b_key) ? -1 : 1; + } ]); + return $result; } @@ -208,11 +261,31 @@ class Entry extends Model */ public function getInternalAttributes(): Collection { - return collect($this->getAttributes())->filter(function($item) { + return $this->objects->filter(function($item) { return $item->is_internal; }); } + /** + * Get an attribute as an object + * + * @param string $key + * @return Attribute|null + */ + public function getObject(string $key): Attribute|null + { + return $this->objects->get($this->normalizeAttributeKey($key)); + } + + public function getObjects(): Collection + { + // In case we havent built our objects yet (because they werent available while determining the schema DN) + if ((! $this->objects->count()) && $this->attributes) + $this->objects = $this->getAttributesAsObjects($this->attributes); + + return $this->objects; + } + /** * Return a list of attributes without any values * @@ -230,11 +303,46 @@ class Entry extends Model */ public function getVisibleAttributes(): Collection { - return collect($this->getAttributes())->filter(function($item) { + return $this->objects->filter(function($item) { return ! $item->is_internal; }); } + public function hasAttribute(int|string $key): bool + { + return $this->objects->has($key); + } + + /** + * Export this record + * + * @param string $method + * @param string $scope + * @return string + * @throws \Exception + */ + public function export(string $method,string $scope): string + { + // @todo To implement + switch ($scope) { + case 'base': + case 'one': + case 'sub': + break; + + default: + throw new \Exception('Export scope unknown:'.$scope); + } + + switch ($method) { + case 'ldif': + return new LDIF(collect($this)); + + default: + throw new \Exception('Export method not implemented:'.$method); + } + } + /** * Return an icon for a DN based on objectClass * @@ -300,4 +408,16 @@ class Entry extends Model // Default return 'fa-fw fas fa-cog'; } + + /** + * Dont convert our $this->attributes to $this->objects when creating a new Entry::class + * + * @return $this + */ + public function noObjectAttributes(): static + { + $this->noObjectAttributes = TRUE; + + return $this; + } } \ No newline at end of file diff --git a/htdocs/export.php b/htdocs/export.php deleted file mode 100755 index cc1096b..0000000 --- a/htdocs/export.php +++ /dev/null @@ -1,40 +0,0 @@ -getIndex(),get_request('exporter_id','REQUEST')); -$request['export'] = $request['exporter']->getTemplate(); -$types = $request['export']->getType(); - -# send the header -if ($request['file']) { - $obStatus = ob_get_status(); - if (isset($obStatus['type']) && $obStatus['type'] && $obStatus['status']) - ob_end_clean(); - - header('Content-type: application/download'); - header(sprintf('Content-Disposition: inline; filename="%s.%s"','export',$types['extension'].($request['export']->isCompressed() ? '.gz' : ''))); - echo $request['export']->export(); - die(); - -} else { - print '
';
-	echo htmlspecialchars($request['export']->export());
-	print '
'; -} -?> diff --git a/htdocs/import_form.php b/htdocs/import_form.php deleted file mode 100644 index 9c6bbbb..0000000 --- a/htdocs/import_form.php +++ /dev/null @@ -1,48 +0,0 @@ -getIndex(),get_request('template','REQUEST',false,'none')); -$request['page']->drawTitle(sprintf('%s',_('Import'))); -$request['page']->drawSubTitle(sprintf('%s: %s',_('Server'),$app['server']->getName())); - -echo '
'; -echo '
'; -printf('',$app['server']->getIndex()); -echo ''; -echo '
'; - -echo ''; - -echo ''; -echo ''; -printf('',_('Select an LDIF file')); -echo ''; - -printf('',_('Maximum file size'),ini_get('upload_max_filesize')); - -echo ''; -printf('',_('Or paste your LDIF here')); -echo ''; -echo ''; -printf('', - _("Don't stop on errors")); -printf('',_('Proceed >>')); -echo '
 
%s'; -echo ''; -echo '
 %s %s
 
%s
 
 %s
 
'; -echo '
'; -?> diff --git a/lib/export_functions.php b/lib/export_functions.php deleted file mode 100644 index 7f08b87..0000000 --- a/lib/export_functions.php +++ /dev/null @@ -1,643 +0,0 @@ -server_id = $server_id; - $this->template_id = $template_id; - - $this->accept(); - } - - static function types() { - $type = array(); - - $details = ExportCSV::getType(); - $type[$details['type']] = $details; - $details = ExportDSML::getType(); - $type[$details['type']] = $details; - $details = ExportLDIF::getType(); - $type[$details['type']] = $details; - $details = ExportVCARD::getType(); - $type[$details['type']] = $details; - - return $type; - } - - private function accept() { - switch($this->template_id) { - case 'CSV': - $this->template = new ExportCSV(); - break; - - case 'DSML': - $this->template = new ExportDSML(); - break; - - case 'LDIF': - $this->template = new ExportLDIF(); - break; - - case 'VCARD': - $this->template = new ExportVCARD(); - break; - - default: - system_message(array( - 'title'=>sprintf('%s %s',_('Unknown Export Type'),$this->template_id), - 'body'=>_('phpLDAPadmin has not been configured for that export type'), - 'type'=>'warn'),'index.php'); - die(); - } - - $this->template->accept(); - } - - public function getTemplate() { - return $this->template; - } -} - -/** - * Export Class - * - * This abstract classes provides all the common methods and variables for the - * custom export classes. - * - * @package phpLDAPadmin - * @subpackage Export - */ -abstract class Export { - # Line Break - protected $br; - # Compress the output - protected $compress; - # Export Results - protected $results; - protected $resultsdata; - protected $items = 0; - - /** - * Return this LDAP Server object - * - * @return object DataStore Server - */ - protected function getServer() { - return $_SESSION[APPCONFIG]->getServer($this->getServerID()); - } - - /** - * Return the LDAP server ID - * - * @return int Server ID - */ - protected function getServerID() { - return get_request('server_id','REQUEST'); - } - - public function accept() { - $server = $this->getServer(); - - # Get the data to be exported - $query = array(); - $base = get_request('dn','REQUEST'); - $query['baseok'] = true; - $query['filter'] = get_request('filter','REQUEST',false,'objectclass=*'); - $query['scope'] = get_request('scope','REQUEST',false,'base'); - $query['deref'] = $_SESSION[APPCONFIG]->getValue('deref','export'); - $query['size_limit'] = 0; - $attrs = get_request('attributes','REQUEST'); - - $attrs = preg_replace('/\s+/','',$attrs); - if ($attrs) - $query['attrs'] = explode(',',$attrs); - else - $query['attrs'] = array('*'); - - if (get_request('sys_attr')) { - if (! in_array('*',$query['attrs'])) - array_push($query['attrs'],'*'); - array_push($query['attrs'],'+'); - } - - if (! $base) - $bases = $server->getBaseDN(); - else - $bases = array($base); - - foreach ($bases as $base) { - $query['base'] = $base; - - $time_start = utime(); - $this->results[$base] = $server->query($query,null); - $time_end = utime(); - - usort($this->results[$base],'pla_compare_dns'); - $this->resultsdata[$base]['time'] = round($time_end-$time_start,2); - - # If no result, there is a something wrong - if (! $this->results[$base] && $server->getErrorNum(null)) - system_message(array( - 'title'=>_('Encountered an error while performing search.'), - 'body'=>ldap_error_msg($server->getErrorMessage(null),$server->getErrorNum(null)), - 'type'=>'error')); - - $this->items += count($this->results[$base]); - } - - $this->resultsdata['scope'] = $query['scope']; - $this->resultsdata['filter'] = $query['filter']; - $this->resultsdata['attrs'] = $query['attrs']; - - # Other settings - switch (get_request('format','POST',false,'unix')) { - case 'win': - $this->br = "\r\n"; - break; - - case 'mac': - $this->br = "\r"; - break; - - case 'unix': - default: - $this->br = "\n"; - } - - if (get_request('compress','REQUEST') == 'on') - $this->compress = true; - } - - public function isCompressed() { - return $this->compress; - } - - protected function getHeader() { - $server = $this->getServer(); - $type = $this->getType(); - - $output = ''; - - $output .= sprintf('# %s %s %s%s',$type['description'],_('for'),implode('|',array_keys($this->results)),$this->br); - $output .= sprintf('# %s: %s (%s)%s',_('Server'),$server->getName(),$server->getValue('server','host'),$this->br); - $output .= sprintf('# %s: %s%s',_('Search Scope'),$this->resultsdata['scope'],$this->br); - $output .= sprintf('# %s: %s%s',_('Search Filter'),$this->resultsdata['filter'],$this->br); - $output .= sprintf('# %s: %s%s',_('Total Entries'),$this->items,$this->br); - $output .= sprintf('#%s',$this->br); - $output .= sprintf('# Generated by %s (%s) on %s%s',app_name(),get_href('web'),date('F j, Y g:i a'),$this->br); - $output .= sprintf('# Version: %s%s',app_version(),$this->br); - - $output .= $this->br; - - return $output; - } - - /** - * Helper method to check if the attribute value should be base 64 encoded. - * - * @param The string to check. - * @return boolean true if the string is safe ascii, false otherwise. - */ - protected function isSafeAscii($str) { - for ($i=0;$i 127) - return false; - - return true; - } -} - -/** - * Export entries to CSV - * - * @package phpLDAPadmin - * @subpackage Export - */ -class ExportCSV extends Export { - private $separator = ','; - private $qualifier = '"'; - private $multivalue_separator = ' | '; - private $escapeCode = '"'; - - static public function getType() { - return array('type'=>'CSV','description' => 'CSV (Spreadsheet)','extension'=>'csv'); - } - - function export() { - $server = $this->getServer(); - - /* Go thru and find all the attribute names first. This is needed, because, otherwise we have - * no idea as to which search attributes were actually populated with data */ - $headers = array('dn'); - $entries = array(); - foreach ($this->results as $base => $results) { - foreach ($results as $dndetails) { - array_push($entries,$dndetails); - - unset($dndetails['dn']); - foreach (array_keys($dndetails) as $key) - if (! in_array($key,$headers)) - array_push($headers,$key); - - } - } - - $output = ''; - $num_headers = count($headers); - - # Print out the headers - for ($i=0; $i<$num_headers; $i++) { - $output .= sprintf('%s%s%s',$this->qualifier,$headers[$i],$this->qualifier); - - if ($i < $num_headers-1) - $output .= $this->separator; - } - - # Drop out our DN header. - array_shift($headers); - $num_headers--; - - $output .= $this->br; - - # Loop on every entry - foreach ($entries as $index => $entry) { - $dn = $entry['dn']; - unset($entry['dn']); - $output .= sprintf('%s%s%s%s',$this->qualifier,$this->LdapEscape($dn),$this->qualifier,$this->separator); - - # Print the attributes - for ($j=0; $j<$num_headers; $j++) { - $attr = $headers[$j]; - $output .= $this->qualifier; - - if (array_key_exists($attr,$entry)) { - $binary_attribute = $server->isAttrBinary($attr) ? 1 : 0; - - if (! is_array($entry[$attr])) - $attr_values = array($entry[$attr]); - else - $attr_values = $entry[$attr]; - - $num_attr_values = count($attr_values); - - for ($i=0; $i<$num_attr_values; $i++) { - if ($binary_attribute) - $output .= base64_encode($attr_values[$i]); - else - $output .= $this->LdapEscape($attr_values[$i]); - - if ($i < $num_attr_values-1) - $output .= $this->multivalue_separator; - } - } - - $output .= $this->qualifier; - - if ($j < $num_headers-1) - $output .= $this->separator; - } - - $output .= $this->br; - } - - if ($this->compress) - return gzencode($output); - else - return $output; - } - - /** - * Function to escape data, where the qualifier happens to also - * be in the data. - */ - private function LdapEscape ($var) { - return str_replace($this->qualifier,$this->escapeCode.$this->qualifier,$var); - } -} - -/** - * Export entries to DSML v.1 - * - * @package phpLDAPadmin - * @subpackage Export - */ -class ExportDSML extends Export { - static public function getType() { - return array('type'=>'DSML','description' => _('DSML V.1 Export'),'extension'=>'xml'); - } - - /** - * Export entries to DSML format - */ - function export() { - $server = $this->getServer(); - - # Not very elegant, but do the job for the moment as we have just 4 level - $indent = array(); - $indent['dir'] = ' '; - $indent['ent'] = ' '; - $indent['att'] = ' '; - $indent['val'] = ' '; - - # Print declaration - $output = sprintf('%s',$this->br); - - # Print root element - $output .= sprintf('%s',$this->br); - - # Print info related to this export - $output .= sprintf('%s',$this->br); - $output .= $this->br; - - $output .= sprintf('%s%s',$indent['dir'],$this->br); - - # Sift through the entries. - $counter = 0; - foreach ($this->results as $base => $results) { - foreach ($results as $dndetails) { - $counter++; - - $dn = $dndetails['dn']; - unset($dndetails['dn']); - ksort($dndetails); - - # Display DN - $output .= sprintf('%s%s',$indent['ent'],htmlspecialchars($dn),$this->br); - - # Display the objectClass attributes first - if (isset($dndetails['objectClass'])) { - if (! is_array($dndetails['objectClass'])) - $dndetails['objectClass'] = array($dndetails['objectClass']); - - $output .= sprintf('%s%s',$indent['att'],$this->br); - - foreach ($dndetails['objectClass'] as $ocValue) - $output .= sprintf('%s%s%s',$indent['val'],$ocValue,$this->br); - - $output .= sprintf('%s%s',$indent['att'],$this->br); - unset($dndetails['objectClass']); - } - - # Display the attributes - foreach ($dndetails as $key => $attr) { - if (! is_array($attr)) - $attr = array($attr); - - $output .= sprintf('%s%s',$indent['att'],$key,$this->br); - - # If the attribute is binary, set the flag $binary_mode to true - $binary_mode = $server->isAttrBinary($key) ? 1 : 0; - - foreach ($attr as $value) - $output .= sprintf('%s%s%s', - $indent['val'],($binary_mode ? base64_encode($value) : htmlspecialchars($value)),$this->br); - - $output .= sprintf('%s%s',$indent['att'],$this->br); - } - - $output .= sprintf('%s%s',$indent['ent'],$this->br); - } - } - - $output .= sprintf('%s%s',$indent['dir'],$this->br); - $output .= sprintf('%s',$this->br); - - if ($this->compress) - return gzencode($output); - else - return $output; - } -} - -/** - * Export from LDAP using an LDIF format - * - * @package phpLDAPadmin - * @subpackage Export - */ -class ExportLDIF extends Export { - # The maximum length of the ldif line - private $line_length = 76; - - static public function getType() { - return array('type'=>'LDIF','description' => _('LDIF Export'),'extension'=>'ldif'); - } - - /** - * Export entries to LDIF format - */ - public function export() { - if (! $this->results) { - echo _('Nothing to export'); - return; - } - - $server = $this->getServer(); - - $output = $this->getHeader(); - - # Add our version. - $output .= 'version: 1'; - $output .= $this->br; - $output .= $this->br; - - # Sift through the entries. - $counter = 0; - foreach ($this->results as $base => $results) { - foreach ($results as $dndetails) { - $counter++; - - $dn = $dndetails['dn']; - unset($dndetails['dn']); - ksort($dndetails); - - $title_string = sprintf('# %s %s: %s%s',_('Entry'),$counter,$dn,$this->br); - - if (strlen($title_string) > $this->line_length-3) - $title_string = substr($title_string,0,$this->line_length-3).'...'.$this->br; - - $output .= $title_string; - - # Display dn - if ($this->isSafeAscii($dn)) - $output .= $this->multiLineDisplay(sprintf('dn: %s',$dn)); - else - $output .= $this->multiLineDisplay(sprintf('dn:: %s',base64_encode($dn))); - - # display the attributes - foreach ($dndetails as $key => $attr) { - if (! is_array($attr)) - $attr = array($attr); - - foreach ($attr as $value) - if (! $this->isSafeAscii($value) || $server->isAttrBinary($key)) - $output .= $this->multiLineDisplay(sprintf('%s:: %s',$key,base64_encode($value))); - else - $output .= $this->multiLineDisplay(sprintf('%s: %s',$key,$value)); - } - - $output .= $this->br; - } - } - - if ($this->compress) - return gzencode($output); - else - return $output; - } - - /** - * Helper method to wrap ldif lines - * - * @param The line to be wrapped if needed. - */ - private function multiLineDisplay($str) { - $length_string = strlen($str); - $length_max = $this->line_length; - - $output = ''; - while ($length_string > $length_max) { - $output .= substr($str,0,$length_max).$this->br.' '; - $str = substr($str,$length_max,$length_string); - $length_string = strlen($str); - - /* Need to do minus one to align on the right - * the first line with the possible following lines - * as these will have an extra space. */ - $length_max = $this->line_length-1; - } - - $output .= $str.$this->br; - - return $output; - } -} - -/** - * Export entries to VCARD v2.1 - * - * @package phpLDAPadmin - * @subpackage Export - */ -class ExportVCARD extends Export { - static public function getType() { - return array('type'=>'VCARD','description' => _('VCARD 2.1 Export'),'extension'=>'vcf'); - } - - # Mappping one to one attribute - private $mapping = array( - 'cn' => 'FN', - 'title' => 'TITLE', - 'homephone' => 'TEL;HOME', - 'mobile' => 'TEL;CELL', - 'mail' => 'EMAIL;Internet', - 'labeleduri' =>'URL', - 'o' => 'ORG', - 'audio' => 'SOUND', - 'facsmiletelephoneNumber' =>'TEL;WORK;HOME;VOICE;FAX', - 'jpegphoto' => 'PHOTO;ENCODING=BASE64', - 'businesscategory' => 'ROLE', - 'description' => 'NOTE' - ); - - private $deliveryAddress = array( - 'postofficebox', - 'street', - 'l', - 'st', - 'postalcode', - 'c'); - - /** - * Export entries to VCARD format - */ - function export() { - $server = $this->getServer(); - $output = ''; - - # Sift through the entries. - foreach ($this->results as $base => $results) { - foreach ($results as $dndetails) { - $dndetails = array_change_key_case($dndetails); - - # Check the attributes needed for the delivery address field - $addr = 'ADR:'; - foreach ($this->deliveryAddress as $attr) { - if (isset($dndetails[$attr])) { - $addr .= $dndetails[$attr]; - unset($dndetails[$attr]); - } - - $addr .= ';'; - } - - $output .= sprintf('BEGIN:VCARD%s',$this->br); - - # Loop for the attributes - foreach ($dndetails as $key => $attr) { - if (! is_array($attr)) - $attr = array($attr); - - # If an attribute of the ldap entry exist in the mapping array for vcard - if (isset($this->mapping[$key])) { - - # Case of organisation. Need to append the possible ou attribute - if ($key == 'o') { - $output .= sprintf('%s:%s',$this->mapping[$key],$attr[0]); - - if (isset($entry['ou'])) - foreach ($entry['ou'] as $ou_value) - $output .= sprintf(';%s',$ou_value); - - # The attribute is binary. (to do : need to fold the line) - } elseif (in_array($key,array('audio','jpegphoto'))) { - $output .= $this->mapping[$key].':'.$this->br; - $output .= ' '.base64_encode($attr[0]); - - } else { - $output .= $this->mapping[$key].':'.$attr[0]; - } - - $output .= $this->br; - } - } - - $output .= sprintf('UID:%s%s',isset($dndetails['entryUUID']) ? $dndetails['entryUUID'] : $dndetails['dn'],$this->br); - $output .= sprintf('VERSION:2.1%s',$this->br); - $output .= sprintf('%s%s',$addr,$this->br); - $output .= sprintf('END:VCARD%s',$this->br); - } - } - - if ($this->compress) - return gzencode($output); - else - return $output; - } -} -?> diff --git a/public/css/fixes.css b/public/css/fixes.css index bd4748d..ea7e78e 100644 --- a/public/css/fixes.css +++ b/public/css/fixes.css @@ -240,4 +240,32 @@ p { /** Force our validation color to have a higher priority than any specified border **/ .was-validated .form-control:invalid, .form-control.is-invalid { border-color: #d92550 !important; +} + +.text-monospace { + font-family: monospace; +} + +pre { + padding:5px; + white-space: -moz-pre-wrap; /* Mozilla, supported since 1999 */ + white-space: -pre-wrap; /* Opera */ + white-space: -o-pre-wrap; /* Opera */ + white-space: pre-wrap; /* CSS3 – Text module (Candidate Recommendation) http://www.w3.org/TR/css3-text/#white-space */ + word-wrap: break-word; /* IE 5.5+ */ +} +pre code { + counter-reset: line-numbering; +} + +pre code .line::before { + content: counter(line-numbering); + counter-increment: line-numbering; + padding-right: .8em; /* space after numbers */ + margin-right: 1em; + width: 4em; + text-align: right; + opacity: 0.5; + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, .5); } \ No newline at end of file diff --git a/resources/themes/architect/views/layouts/app.blade.php b/resources/themes/architect/views/layouts/app.blade.php index ce44b8b..b5ff7e6 100644 --- a/resources/themes/architect/views/layouts/app.blade.php +++ b/resources/themes/architect/views/layouts/app.blade.php @@ -35,6 +35,8 @@ + @yield('page-modals') + @section('scripts') @include('architect::layouts.partials.scripts') diff --git a/resources/themes/architect/views/layouts/partials/contentheader.blade.php b/resources/themes/architect/views/layouts/partials/contentheader.blade.php index b1bc046..e4308d0 100644 --- a/resources/themes/architect/views/layouts/partials/contentheader.blade.php +++ b/resources/themes/architect/views/layouts/partials/contentheader.blade.php @@ -27,23 +27,31 @@
@@ -106,6 +112,20 @@
- + -@endsection \ No newline at end of file +@endsection + +@section('page-scripts') + +@append \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 93bff74..f5472a9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,7 +2,7 @@ use Illuminate\Support\Facades\Route; -use App\Http\Controllers\HomeController; +use App\Http\Controllers\{HomeController,ImportController}; use App\Http\Controllers\Auth\LoginController; /* @@ -30,6 +30,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale()], function() { Route::get('info',[HomeController::class,'info']); Route::post('dn',[HomeController::class,'dn_frame']); Route::get('debug',[HomeController::class,'debug']); + Route::get('import',[HomeController::class,'import_frame']); Route::get('schema',[HomeController::class,'schema_frame']); }); @@ -41,4 +42,7 @@ Route::group(['prefix'=>'user'],function() { Route::post('entry/update/commit',[HomeController::class,'entry_update']); Route::post('entry/update/pending',[HomeController::class,'entry_pending_update']); -Route::get('entry/newattr/{id}',[HomeController::class,'entry_newattr']); \ No newline at end of file +Route::get('entry/newattr/{id}',[HomeController::class,'entry_newattr']); +Route::get('entry/export/{id}',[HomeController::class,'entry_export']); + +Route::post('import/process/{type}',[HomeController::class,'import']); \ No newline at end of file