Start of implementation of Import and Export using LDIF

This commit is contained in:
Deon George 2024-01-11 08:59:40 +11:00
parent ded1f74285
commit 4c8bd1c81f
30 changed files with 1118 additions and 925 deletions

View File

@ -168,6 +168,11 @@ class Attribute implements \Countable, \ArrayAccess
return $this->name; return $this->name;
} }
public function addValue(string $value): void
{
$this->values->push($value);
}
public function count(): int public function count(): int
{ {
return $this->values->count(); return $this->values->count();

View File

@ -50,7 +50,7 @@ class Factory
*/ */
public static function create(string $attribute,array $values): Attribute 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)); Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class));
return new $class($attribute,$values); return new $class($attribute,$values);

View File

@ -0,0 +1,53 @@
<?php
namespace App\Classes\LDAP;
use Illuminate\Support\Facades\Auth;
use LdapRecord\Query\Collection;
/**
* Export Class
*
* This abstract classes provides all the common methods and variables for the
* export classes.
*/
abstract class Export
{
// Line Break
protected string $br = "\r\n";
// Item(s) being Exported
protected Collection $items;
// Type of export
protected const type = 'Unknown';
public function __construct(Collection $items)
{
$this->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;
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Classes\LDAP\Export;
use Illuminate\Support\Str;
use App\Classes\LDAP\Export;
/**
* Export from LDAP using an LDIF format
*/
class LDIF extends Export
{
// The maximum length of the ldif line
private int $line_length = 76;
protected const type = 'LDIF Export';
public function __toString(): string
{
$result = parent::header();
$result .= 'version: 1';
$result .= $this->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;
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Classes\LDAP;
use Illuminate\Support\Collection;
use App\Exceptions\Import\GeneralException;
use App\Exceptions\Import\ObjectExistsException;
use App\Ldap\Entry;
/**
* Import Class
*
* This abstract classes provides all the common methods and variables for the
* import classes.
*/
abstract class Import
{
// Valid LDIF commands
protected const LDAP_IMPORT_ADD = 1;
protected const LDAP_IMPORT_DELETE = 2;
protected const LDAP_IMPORT_MODRDN = 3;
protected const LDAP_IMPORT_MODDN = 4;
protected const LDAP_IMPORT_MODIFY = 5;
protected const LDAP_ACTIONS = [
'add' => 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;
}

View File

@ -0,0 +1,233 @@
<?php
namespace App\Classes\LDAP\Import;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Nette\NotImplementedException;
use App\Classes\LDAP\Import;
use App\Exceptions\Import\{GeneralException,VersionException};
use App\Ldap\Entry;
/**
* Import LDIF to LDAP using an LDIF format
*
* The LDIF spec is described by RFC2849
* http://www.ietf.org/rfc/rfc2849.txt
*/
class LDIF extends Import
{
private const LOGKEY = 'ILF';
public function process(): Collection
{
$c = 0;
$action = NULL;
$attribute = NULL;
$base64encoded = FALSE;
$o = NULL;
$value = '';
$version = NULL;
$result = collect();
// @todo When renaming DNs, the hotlink should point to the new entry on success, or the old entry on failure.
foreach (preg_split('/(\r?\n|\r)/',$this->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;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Import;
use Exception;
class AttributeException extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Import;
use Exception;
class GeneralException extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Import;
use Exception;
class ObjectExistsException extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Import;
use Exception;
class VersionException extends Exception {}

View File

@ -8,19 +8,38 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;
use LdapRecord\Exceptions\InsufficientAccessException; use LdapRecord\Exceptions\InsufficientAccessException;
use LdapRecord\LdapRecordException; use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException; use LdapRecord\Query\ObjectNotFoundException;
use App\Classes\LDAP\{Attribute,Server}; use App\Classes\LDAP\{Attribute,Server};
use App\Classes\LDAP\Import\LDIF as LDIFImport;
use App\Classes\LDAP\Export\LDIF as LDIFExport;
use App\Exceptions\Import\{GeneralException,VersionException};
use App\Exceptions\InvalidUsage; use App\Exceptions\InvalidUsage;
use App\Http\Requests\EntryRequest; use App\Http\Requests\{EntryRequest,ImportRequest};
use App\Ldap\Entry;
use App\View\Components\AttributeType; use App\View\Components\AttributeType;
use Nette\NotImplementedException;
class HomeController extends Controller class HomeController extends Controller
{ {
private function bases()
{
$base = Server::baseDNs() ?: collect();
return $base->transform(function($item) {
return [
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
];
});
}
/** /**
* Debug Page * Debug Page
* *
@ -49,6 +68,22 @@ class HomeController extends Controller
->with('page_actions',$page_actions); ->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) public function entry_newattr(string $id)
{ {
$x = new AttributeType(new Attribute($id,[]),TRUE); $x = new AttributeType(new Attribute($id,[]),TRUE);
@ -76,20 +111,8 @@ class HomeController extends Controller
->withInput() ->withInput()
->with('note',__('No attributes changed')); ->with('note',__('No attributes changed'));
$base = Server::baseDNs() ?: collect(); return view('update')
->with('bases',$this->bases())
$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)
->with('dn',$dn) ->with('dn',$dn)
->with('o',$o); ->with('o',$o);
} }
@ -103,18 +126,6 @@ class HomeController extends Controller
*/ */
public function entry_update(EntryRequest $request) 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); $dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn); $o = config('server')->fetch($dn);
@ -168,51 +179,75 @@ class HomeController extends Controller
*/ */
public function home() 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')) if (old('dn'))
return view('frame') return view('frame')
->with('subframe','dn') ->with('subframe','dn')
->with('bases',$bases) ->with('bases',$this->bases())
->with('o',config('server')->fetch($dn=Crypt::decryptString(old('dn')))) ->with('o',config('server')->fetch($dn=Crypt::decryptString(old('dn'))))
->with('dn',$dn); ->with('dn',$dn);
elseif (old('frame')) elseif (old('frame'))
return view('frame') return view('frame')
->with('subframe',old('frame')) ->with('subframe',old('frame'))
->with('bases',$bases); ->with('bases',$this->bases());
else else
return view('home') return view('home')
->with('bases',$bases) ->with('bases',$this->bases())
->with('server',config('ldap.connections.default.name')); ->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 * LDAP Server INFO
* *
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws ObjectNotFoundException
*/ */
public function info() public function info()
{ {
// Load our attributes
$s = config('server');
$s->schema('objectclasses');
$s->schema('attributetypes');
return view('frames.info') return view('frames.info')
->with('s',$s); ->with('s',config('server'));
} }
/** /**

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ImportRequest extends FormRequest
{
public function authorize()
{
return TRUE;
}
public function rules()
{
return [
'frame' => 'required|string|in:import',
'file' => 'nullable|extensions:ldif|required_without:text',
'text'=> 'nullable|prohibits:file|string|min:16',
];
}
}

View File

@ -2,21 +2,50 @@
namespace App\Ldap; namespace App\Ldap;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use LdapRecord\Support\Arr;
use LdapRecord\Models\Model; use LdapRecord\Models\Model;
use LdapRecord\Query\Model\Builder;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute\Factory; use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\Export\LDIF;
use App\Exceptions\Import\AttributeException;
class Entry extends Model class Entry extends Model
{ {
private Collection $objects;
private bool $noObjectAttributes = FALSE;
/* OVERRIDES */ /* 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 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 protected function originalIsEquivalent(string $key): bool
{ {
if (! array_key_exists($key, $this->original)) { $key = $this->normalizeAttributeKey($key);
return false;
if ((! array_key_exists($key, $this->original)) && (! $this->objects->has($key))) {
return TRUE;
} }
$current = $this->attributes[$key]; $current = $this->attributes[$key];
$original = $this->original[$key]; $original = $this->objects->get($key)->values;
if ($current === $original) { if ($current === $original) {
return true; return true;
} }
//dump(['key'=>$key,'current'=>$current,'original'=>$this->original[$key],'objectvalue'=>$this->getAttributeAsObject($key)->isDirty()]); return ! $this->getObject($key)->isDirty();
return ! $this->getAttributeAsObject($key)->isDirty();
} }
public function getOriginal(): array public static function query(bool $noattrs=false): Builder
{ {
static $result = NULL; $o = new static;
if (is_null($result)) { if ($noattrs)
$result = collect(); $o->noObjectAttributes();
// @todo Optimise this foreach with getAttributes() return $o->newQuery();
foreach (parent::getOriginal() as $attribute => $value) { }
// If the attribute name has language tags
$matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
// If the attribute doesnt exist we'll create it /**
$o = Arr::get($result,$attribute,Factory::create($attribute,[])); * As attribute values are updated, or new ones created, we need to mirror that
$o->setLangTag($matches[3],$value); * into our $objects
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function setAttribute(string $key, mixed $value): static
{
parent::setAttribute($key,$value);
} else { $key = $this->normalizeAttributeKey($key);
$o = Factory::create($attribute,$value);
}
if (! $result->has($attribute)) { if ((! $this->objects->get($key)) && $value) {
// Set the rdn flag $o = new Attribute($key,[]);
if (preg_match('/^'.$attribute.'=/i',$this->dn)) $o->value = $value;
$o->setRDN();
// Set required flag $this->objects->put($key,$o);
$o->required_by(collect($this->getAttribute('objectclass')));
$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 */ /* ATTRIBUTES */
@ -92,88 +144,89 @@ class Entry extends Model
/* METHODS */ /* METHODS */
/** public function addAttribute(string $key,mixed $value): void
* Get an attribute as an object
*
* @param string $key
* @return Attribute|null
*/
public function getAttributeAsObject(string $key): Attribute|null
{ {
return Arr::get($this->getAttributesAsObjects(),$key); $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 * Convert all our attribute values into an array of Objects
* *
* @param array $attributes
* @return Collection * @return Collection
*/ */
protected function getAttributesAsObjects(): Collection protected function getAttributesAsObjects(array $attributes): Collection
{ {
static $result = NULL; $result = collect();
if (is_null($result)) { foreach ($attributes as $attribute => $value) {
$result = collect(); // 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 doesnt exist we'll create it
// If the attribute name has language tags $o = Arr::get($result,$attribute,Factory::create($attribute,[]));
$matches = []; $o->setLangTag($matches[3],$value);
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
// If the attribute doesnt exist we'll create it } else {
$o = Arr::get($result,$attribute,Factory::create($attribute,[])); $o = Factory::create($attribute,$value);
$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);
}
} }
$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 // Set required flag
$result = $result->sortBy([function(Attribute $a,Attribute $b) use ($sort): int { $o->required_by(collect($this->getAttribute('objectclass')));
if ($a === $b)
return 0;
// Check if $a/$b are in the configuration to be sorted first, if so get it's key // Store our original value to know if this attribute has changed
$a_key = $sort->search($a->name_lc); if ($x=Arr::get($this->original,$attribute))
$b_key = $sort->search($b->name_lc); $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) $result->put($attribute,$o);
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;
} ]);
} }
$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; return $result;
} }
@ -208,11 +261,31 @@ class Entry extends Model
*/ */
public function getInternalAttributes(): Collection public function getInternalAttributes(): Collection
{ {
return collect($this->getAttributes())->filter(function($item) { return $this->objects->filter(function($item) {
return $item->is_internal; 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 * Return a list of attributes without any values
* *
@ -230,11 +303,46 @@ class Entry extends Model
*/ */
public function getVisibleAttributes(): Collection public function getVisibleAttributes(): Collection
{ {
return collect($this->getAttributes())->filter(function($item) { return $this->objects->filter(function($item) {
return ! $item->is_internal; 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 * Return an icon for a DN based on objectClass
* *
@ -300,4 +408,16 @@ class Entry extends Model
// Default // Default
return 'fa-fw fas fa-cog'; 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;
}
} }

View File

@ -1,40 +0,0 @@
<?php
/**
* Performs the export of data from the LDAP server
*
* @package phpLDAPadmin
* @subpackage Page
*/
/**
*/
require './common.php';
require LIBDIR.'export_functions.php';
# Prevent script from bailing early for long search
@set_time_limit(0);
$request = array();
$request['file'] = get_request('save_as_file') ? true : false;
$request['exporter'] = new Exporter($app['server']->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 '<span style="font-size: 14px; font-family: courier;"><pre>';
echo htmlspecialchars($request['export']->export());
print '</pre></span>';
}
?>

View File

@ -1,48 +0,0 @@
<?php
/**
* Displays a form to allow the user to upload and import
* an LDIF file.
*
* @package phpLDAPadmin
* @subpackage Page
*/
/**
*/
require './common.php';
if (! ini_get('file_uploads'))
error(_('Your PHP.INI does not have file_uploads = ON. Please enable file uploads in PHP.'),'error','index.php');
$request['page'] = new PageRender($app['server']->getIndex(),get_request('template','REQUEST',false,'none'));
$request['page']->drawTitle(sprintf('<b>%s</b>',_('Import')));
$request['page']->drawSubTitle(sprintf('%s: <b>%s</b>',_('Server'),$app['server']->getName()));
echo '<form action="cmd.php" method="post" class="new_value" enctype="multipart/form-data">';
echo '<div>';
printf('<input type="hidden" name="server_id" value="%s" />',$app['server']->getIndex());
echo '<input type="hidden" name="cmd" value="import" />';
echo '</div>';
echo '<table class="forminput" border="0" style="margin-left: auto; margin-right: auto;">';
echo '<tr><td colspan="2">&nbsp;</td></tr>';
echo '<tr>';
printf('<td>%s</td>',_('Select an LDIF file'));
echo '<td>';
echo '<input type="file" name="ldif_file" />';
echo '</td></tr>';
printf('<tr><td>&nbsp;</td><td class="small"><b>%s %s</b></td></tr>',_('Maximum file size'),ini_get('upload_max_filesize'));
echo '<tr><td colspan="2">&nbsp;</td></tr>';
printf('<tr><td>%s</td></tr>',_('Or paste your LDIF here'));
echo '<tr><td colspan="2"><textarea name="ldif" rows="20" cols="100"></textarea></td></tr>';
echo '<tr><td colspan="2">&nbsp;</td></tr>';
printf('<tr><td>&nbsp;</td><td class="small"><input type="checkbox" name="continuous_mode" value="1" />%s</td></tr>',
_("Don't stop on errors"));
printf('<tr><td>&nbsp;</td><td><input type="submit" value="%s" /></td></tr>',_('Proceed >>'));
echo '</table>';
echo '</form>';
?>

View File

@ -1,643 +0,0 @@
<?php
/**
* Classes and functions for export data from LDAP
*
* These classes provide differnet export formats.
*
* @author The phpLDAPadmin development team
* @package phpLDAPadmin
* @see export.php and export_form.php
*/
/**
* Exporter Class
*
* This class serves as a top level exporter class, which will return
* the correct Export class.
*
* @package phpLDAPadmin
* @subpackage Export
*/
class Exporter {
# Server ID that the export is linked to
private $server_id;
# Exporter Type
private $template_id;
private $template;
public function __construct($server_id,$template_id) {
$this->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<strlen($str);$i++)
if (ord($str[$i]) < 32 || ord($str[$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('<?xml version="1.0"?>%s',$this->br);
# Print root element
$output .= sprintf('<dsml>%s',$this->br);
# Print info related to this export
$output .= sprintf('<!--%s',$this->br);
$output .= $this->getHeader();
$output .= sprintf('-->%s',$this->br);
$output .= $this->br;
$output .= sprintf('%s<directory-entries>%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<entry dn="%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<objectClass>%s',$indent['att'],$this->br);
foreach ($dndetails['objectClass'] as $ocValue)
$output .= sprintf('%s<oc-value>%s</oc-value>%s',$indent['val'],$ocValue,$this->br);
$output .= sprintf('%s</objectClass>%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<attr name="%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<value>%s</value>%s',
$indent['val'],($binary_mode ? base64_encode($value) : htmlspecialchars($value)),$this->br);
$output .= sprintf('%s</attr>%s',$indent['att'],$this->br);
}
$output .= sprintf('%s</entry>%s',$indent['ent'],$this->br);
}
}
$output .= sprintf('%s</directory-entries>%s',$indent['dir'],$this->br);
$output .= sprintf('</dsml>%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;
}
}
?>

28
public/css/fixes.css vendored
View File

@ -241,3 +241,31 @@ p {
.was-validated .form-control:invalid, .form-control.is-invalid { .was-validated .form-control:invalid, .form-control.is-invalid {
border-color: #d92550 !important; 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);
}

View File

@ -35,6 +35,8 @@
</div> </div>
</div> </div>
@yield('page-modals')
@section('scripts') @section('scripts')
@include('architect::layouts.partials.scripts') @include('architect::layouts.partials.scripts')

View File

@ -27,23 +27,31 @@
<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->search('edit') !== FALSE)) || 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">
<i class="fas fa-fw fa-edit me-2"></i> <i class="fas fa-fw fa-edit me-2"></i> @lang('Edit')
@lang('Edit')
</button> </button>
</span> </span>
</li> </li>
@endif @endif
@if (isset($page_actions) && $page_actions->search('copy') !== FALSE) @if (isset($page_actions) && ($page_actions->search('export') !== FALSE))
<li class="nav-item">
<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"--}}>
<i class="fas fa-fw fa-file-export me-2"></i> @lang('Export')
</button>
</a>
</li>
@endif
@if (isset($page_actions) && ($page_actions->search('copy') !== FALSE))
<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">
<i class="fas fa-fw fa-truck-moving me-2"></i> <i class="fas fa-fw fa-truck-moving me-2"></i> @lang('Copy or Move')
@lang('Copy or Move')
</button> </button>
</a> </a>
</li> </li>

View File

@ -40,6 +40,9 @@
<div class="font-icon-wrapper float-start ms-1 me-1 server-icon"> <div class="font-icon-wrapper float-start ms-1 me-1 server-icon">
<a class="p-0 m-0" href="{{ LaravelLocalization::localizeUrl('schema') }}" onclick="return false;" style="display: contents;"><i class="fas fa-fw fa-fingerprint"></i></a> <a class="p-0 m-0" href="{{ LaravelLocalization::localizeUrl('schema') }}" onclick="return false;" style="display: contents;"><i class="fas fa-fw fa-fingerprint"></i></a>
</div> </div>
<div class="font-icon-wrapper float-start ms-1 me-1 server-icon">
<a class="p-0 m-0" href="{{ LaravelLocalization::localizeUrl('import') }}" onclick="return false;" style="display: contents;"><i class="fas fa-fw fa-upload"></i></a>
</div>
@env(['local']) @env(['local'])
<div class="font-icon-wrapper float-end ms-1 server-icon"> <div class="font-icon-wrapper float-end ms-1 server-icon">
<a class="p-0 m-0" href="{{ LaravelLocalization::localizeUrl('debug') }}" onclick="return false;" style="display: contents;"><i class="fas fa-fw fa-toolbox"></i></a> <a class="p-0 m-0" href="{{ LaravelLocalization::localizeUrl('debug') }}" onclick="return false;" style="display: contents;"><i class="fas fa-fw fa-toolbox"></i></a>

View File

@ -0,0 +1,9 @@
@extends('architect::layouts.error')
@section('title')
@lang('Not Implemented') <small>(555)</small>
@endsection
@section('content')
{{ $exception->getMessage() }}
@endsection

View File

@ -0,0 +1,9 @@
@extends('architect::layouts.error')
@section('title')
@lang('Error') <small>(598)</small>
@endsection
@section('content')
{{ $exception->getMessage() }}
@endsection

View File

@ -0,0 +1,3 @@
<pre>
{{ $result }}
</pre>

View File

@ -3,7 +3,7 @@
@section('page_title') @section('page_title')
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
<td class="{{ ($x=Arr::get($o->getAttributes(),'jpegphoto')) ? 'border' : '' }}" rowspan="2">{!! $x ? $x->render() : sprintf('<div class="page-title-icon f32"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!}</td> <td class="{{ ($x=$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> <td class="text-end align-text-top p-0 {{ $x ? 'ps-5' : 'pt-2' }}"><strong>{{ $dn }}</strong></td>
</tr> </tr>
<tr> <tr>
@ -11,11 +11,11 @@
<table> <table>
<tr> <tr>
<td class="p-1 m-1">Created</td> <td class="p-1 m-1">Created</td>
<th class="p-1 m-1">{{ ($x=Arr::get($o->getAttributes(),'createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=Arr::get($o->getAttributes(),'creatorsname')) ? $x->render() : __('Unknown') }}]</th> <th class="p-1 m-1">{{ ($x=$o->getObject('createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('creatorsname')) ? $x->render() : __('Unknown') }}]</th>
</tr> </tr>
<tr> <tr>
<td class="p-1 m-1">Modified</td> <td class="p-1 m-1">Modified</td>
<th class="p-1 m-1">{{ ($x=Arr::get($o->getAttributes(),'modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=Arr::get($o->getAttributes(),'modifiersname')) ? $x->render() : __('Unknown') }}]</th> <th class="p-1 m-1">{{ ($x=$o->getObject('modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('modifiersname')) ? $x->render() : __('Unknown') }}]</th>
</tr> </tr>
<tr> <tr>
<td class="p-1 m-1">UUID</td> <td class="p-1 m-1">UUID</td>
@ -168,8 +168,43 @@
</div> </div>
@endsection @endsection
@section('page-modals')
<!-- Modal -->
<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-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="entry-export-label">LDIF for {{ $dn }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="entry-export"><div class="fa-3x"><i class="fas fa-spinner fa-pulse fa-sm"></i></div></div>
</div>
<div class="modal-footer">
<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>
</div>
</div>
</div>
</div>
@endsection
@section('page-scripts') @section('page-scripts')
<script type="text/javascript"> <script type="text/javascript">
function download(filename,text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function editmode() { function editmode() {
$('button[id=entry-edit]').addClass('active').removeClass('btn-outline-dark').addClass('btn-outline-light'); $('button[id=entry-edit]').addClass('active').removeClass('btn-outline-dark').addClass('btn-outline-light');
@ -190,17 +225,16 @@
$(document).ready(function() { $(document).ready(function() {
$('#form-reset').click(function() { $('#form-reset').click(function() {
$('#dn-edit')[0].reset(); $('#dn-edit')[0].reset();
}) });
$('#form-submit').click(function() { $('#form-submit').click(function() {
$('#dn-edit')[0].submit(); $('#dn-edit')[0].submit();
}) });
$('#newattr').on('change',function(item) { $('#newattr').on('change',function(item) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
beforeSend: function() { beforeSend: function() {},
},
success: function(data) { success: function(data) {
$('#newattrs').append(data); $('#newattrs').append(data);
}, },
@ -210,7 +244,7 @@
}, },
url: '{{ url('entry/newattr') }}/'+item.target.value, url: '{{ url('entry/newattr') }}/'+item.target.value,
cache: false cache: false
}) });
// Remove the option from the list // Remove the option from the list
$(this).find('[value="'+item.target.value+'"]').remove() $(this).find('[value="'+item.target.value+'"]').remove()
@ -229,6 +263,29 @@
editmode(); editmode();
}); });
$('#entry-export-download').on('click',function(item) {
item.preventDefault();
let ldif = $('#entry-export').find('pre:first'); // update this selector in your local version
download('ldap-export.ldif',ldif.html());
});
$('#entry-export-modal').on('shown.bs.modal', function () {
$.ajax({
type: 'GET',
beforeSend: function() {},
success: function(data) {
$('#entry-export').empty().append(data);
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/export',$o->getDNSecure()) }}/',
cache: false
})
})
@if(old()) @if(old())
editmode(); editmode();
@endif @endif

View File

@ -0,0 +1,65 @@
@extends('layouts.dn')
@section('page_title')
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-upload"></i></div></td>
<td class="top text-start align-text-top p-0 pt-2"><strong>@lang('LDIF Import')</strong><br><small>To Server XXX</small></td>
</tr>
</table>
@endsection
@section('main-content')
<div class="row">
<div class="offset-1 col-10">
<div class="main-card mb-3 card">
<form id="import-form" action="{{ url('import/process/ldif') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="hidden" name="frame" value="import">
<div class="card-header">
@lang('LDIF Import')
</div>
<div class="card-body">
<div class="row">
<div class="col">
<div class="form-group">
<label for="ldif-text" class="pb-2"><strong>@lang('Paste in your LDIF here')</strong></label>
<textarea class="form-control text-monospace @error('text') is-invalid @enderror" name="text" rows="10">{{ old('text') }}</textarea>
<div class="invalid-feedback pb-2">
@error('text')
{{ $message }}
@enderror
</div>
</div>
</div>
</div>
<div class="row pt-5">
<div class="col">
<div class="form-group">
<label for="ldif-file" class="pb-2"><strong>@lang('Or upload LDIF file')</strong></label><br>
<input type="file" class="form-control-file @error('file') is-invalid @enderror" name="file" accept=".ldif"><br>
<small class="form-text text-muted @error('file') is-invalid @enderror">@lang('Maximum file size') <strong>{{ ini_get('upload_max_filesize') }}</strong></small>
<div class="invalid-feedback pb-2">
@error('file')
{{ $message }}
@enderror
</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>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,60 @@
@section('page_title')
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-upload"></i></div></td>
<td class="top text-start align-text-top p-0 pt-2"><strong>@lang('LDIF Import Result')</strong><br><small>To Server XXX</small></td>
</tr>
</table>
@endsection
@section('main-content')
<div class="row">
<div class="offset col-12">
<div class="main-card mb-3 card">
<div class="card-body">
<div class="card-header-tabs">
<ul class="nav nav-tabs">
<li class="nav-item"><a data-bs-toggle="tab" href="#result" class="nav-link active">@lang('Import Result')</a></li>
<li class="nav-item"><a data-bs-toggle="tab" href="#ldif" class="nav-link">@lang('LDIF')</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="result" role="tabpanel">
<table class="table table-borderless">
<thead>
<tr>
<th>@lang('DN')</th>
<th>@lang('Result')</th>
<th class="text-end">@lang('Line')</th>
</tr>
</thead>
@foreach ($result as $item)
<tr>
<td>{{ $item->get('dn') }}</td>
<td>{{ $item->get('result') }}</td>
<td class="text-end">{{ $item->get('line') }}</td>
</tr>
@endforeach
</table>
</div>
<div class="tab-pane" id="ldif" role="tabpanel">
<pre class="code"><code>{{ $ldif }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('page-scripts')
<script type="text/javascript">
$(document).ready(function() {
$('pre code').html(function(index, html) {
return html.trim().replace(/^(.*)$/mg, "<span class=\"line\">$1</span>");
});
});
</script>
@append

View File

@ -4,26 +4,28 @@
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-info"></i></div></td> <td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-info"></i></div></td>
<td class="top text-end align-text-top p-0 pt-2 }}"><strong>@lang('Server Info')</strong><br><small>{{ $s->rootDSE()->entryuuid[0] ?? '' }}</small></td> <td class="top text-end align-text-top p-0 pt-2"><strong>@lang('Server Info')</strong><br><small>{{ $s->rootDSE()->entryuuid[0] ?? '' }}</small></td>
</tr> </tr>
</table> </table>
@endsection @endsection
@section('main-content') @section('main-content')
<div class="bg-white p-3"> <div class="main-card mb-3 card">
<table class="table"> <div class="card-body">
@foreach ($s->rootDSE()->getAttributes() as $attribute => $ao) <table class="table">
<tr> @foreach ($s->rootDSE()->getObjects() as $attribute => $ao)
<th class="w-25"> <tr>
{!! ($x=$s->schema('attributetypes',$attribute)) <th class="w-25">
? sprintf('<a class="attributetype" id="strtolower(%s)" href="%s">%s</a>',$x->name_lc,url('schema/attributetypes',$x->name_lc),$x->name) {!! ($x=$s->schema('attributetypes',$attribute))
: $attribute !!} ? sprintf('<a class="attributetype" id="strtolower(%s)" href="%s">%s</a>',$x->name_lc,url('schema/attributetypes',$x->name_lc),$x->name)
</th> : $attribute !!}
<td> </th>
<x-attribute :edit="false" :o="$ao"/> <td>
</td> <x-attribute :edit="false" :o="$ao"/>
</tr> </td>
@endforeach </tr>
</table> @endforeach
</table>
</div>
</div> </div>
@endsection @endsection

View File

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

View File

@ -1,9 +1,11 @@
@extends('dn') @extends('home')
@section('page_title') @section('page_title')
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
<td class="{{ ($x=Arr::get($o->getOriginal(),'jpegphoto')) ? 'border' : '' }}" rowspan="2">{!! $x ? $x->render() : sprintf('<div class="page-title-icon f32"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!}</td> <td class="{{ ($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> <td class="text-end align-text-top p-0 {{ $x ? 'ps-5' : 'pt-2' }}"><strong>{{ $dn }}</strong></td>
</tr> </tr>
<tr> <tr>
@ -11,11 +13,15 @@
<table> <table>
<tr> <tr>
<td class="p-1 m-1">Created</td> <td class="p-1 m-1">Created</td>
<th class="p-1 m-1">{{ ($x=Arr::get($o->getAttributes(),'createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=Arr::get($o->getAttributes(),'creatorsname')) ? $x->render() : __('Unknown') }}]</th> <th class="p-1 m-1">
{{ ($x=$o->getObject('createtimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('creatorsname')) ? $x->render() : __('Unknown') }}]
</th>
</tr> </tr>
<tr> <tr>
<td class="p-1 m-1">Modified</td> <td class="p-1 m-1">Modified</td>
<th class="p-1 m-1">{{ ($x=Arr::get($o->getAttributes(),'modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=Arr::get($o->getAttributes(),'modifiersname')) ? $x->render() : __('Unknown') }}]</th> <th class="p-1 m-1">
{{ ($x=$o->getObject('modifytimestamp')) ? $x->render() : __('Unknown') }} [{{ ($x=$o->getObject('modifiersname')) ? $x->render() : __('Unknown') }}]
</th>
</tr> </tr>
<tr> <tr>
<td class="p-1 m-1">UUID</td> <td class="p-1 m-1">UUID</td>
@ -62,13 +68,13 @@
@endif @endif
<div class="main-card mb-3 card"> <div class="main-card mb-3 card">
<div class="card-body"> <form id="dn-update" method="POST" class="needs-validation" action="{{ url('entry/update/commit') }}" novalidate>
<div class="row"> @csrf
<div class="col-12 col-lg-6 col-xl-4 mx-auto pt-3">
<form id="dn-edit" method="POST" class="needs-validation" action="{{ url('entry/update/commit') }}" novalidate>
@csrf
<input type="hidden" name="dn" value="{{ $o->getDNSecure() }}"> <input type="hidden" name="dn" value="{{ $o->getDNSecure() }}">
<div class="card-body">
<div class="row">
<div class="col-12 col-lg-6 col-xl-4 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">
@ -96,7 +102,7 @@
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
</form> </div>
</div> </div>
<div class="row pt-3"> <div class="row pt-3">
@ -106,6 +112,20 @@
</div> </div>
</div> </div>
</div> </div>
</div> </form>
</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

@ -2,7 +2,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController; use App\Http\Controllers\{HomeController,ImportController};
use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\LoginController;
/* /*
@ -30,6 +30,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale()], function() {
Route::get('info',[HomeController::class,'info']); Route::get('info',[HomeController::class,'info']);
Route::post('dn',[HomeController::class,'dn_frame']); Route::post('dn',[HomeController::class,'dn_frame']);
Route::get('debug',[HomeController::class,'debug']); Route::get('debug',[HomeController::class,'debug']);
Route::get('import',[HomeController::class,'import_frame']);
Route::get('schema',[HomeController::class,'schema_frame']); Route::get('schema',[HomeController::class,'schema_frame']);
}); });
@ -42,3 +43,6 @@ Route::group(['prefix'=>'user'],function() {
Route::post('entry/update/commit',[HomeController::class,'entry_update']); 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::post('import/process/{type}',[HomeController::class,'import']);