Update import to include changetype add/modify operations
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 2m13s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m19s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 2m13s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m19s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
This commit is contained in:
parent
e667d139b0
commit
dba104105e
@ -190,6 +190,21 @@ class Attribute implements \Countable, \ArrayAccess
|
|||||||
->get($tag,[]),$values)));
|
->get($tag,[]),$values)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this attribute has changes, re-render the attribute values
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDirty(): array
|
||||||
|
{
|
||||||
|
$dirty = [];
|
||||||
|
|
||||||
|
if ($this->isDirty())
|
||||||
|
$dirty = [$this->name_lc => $this->_values->toArray()];
|
||||||
|
|
||||||
|
return $dirty;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the hints about this attribute, ie: RDN, Required, etc
|
* Return the hints about this attribute, ie: RDN, Required, etc
|
||||||
*
|
*
|
||||||
|
@ -56,6 +56,7 @@ abstract class Import
|
|||||||
{
|
{
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case static::LDAP_IMPORT_ADD:
|
case static::LDAP_IMPORT_ADD:
|
||||||
|
case static::LDAP_IMPORT_MODIFY:
|
||||||
try {
|
try {
|
||||||
$o->save();
|
$o->save();
|
||||||
|
|
||||||
@ -65,15 +66,17 @@ abstract class Import
|
|||||||
if ($e->getDetailedError())
|
if ($e->getDetailedError())
|
||||||
return collect([
|
return collect([
|
||||||
'dn'=>$o->getDN(),
|
'dn'=>$o->getDN(),
|
||||||
'result'=>sprintf('%d: %s (%s)',
|
'link'=>$o->getDNSecure(),
|
||||||
|
'result'=>sprintf('%d: %s%s',
|
||||||
($x=$e->getDetailedError())->getErrorCode(),
|
($x=$e->getDetailedError())->getErrorCode(),
|
||||||
$x->getErrorMessage(),
|
$x->getErrorMessage(),
|
||||||
$x->getDiagnosticMessage(),
|
$x->getDiagnosticMessage() ? ' ('.$x->getDiagnosticMessage().')' : '',
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
else
|
else
|
||||||
return collect([
|
return collect([
|
||||||
'dn'=>$o->getDN(),
|
'dn'=>$o->getDN(),
|
||||||
|
'link'=>$o->getDNSecure(),
|
||||||
'result'=>sprintf('%d: %s',
|
'result'=>sprintf('%d: %s',
|
||||||
$e->getCode(),
|
$e->getCode(),
|
||||||
$e->getMessage(),
|
$e->getMessage(),
|
||||||
@ -81,9 +84,13 @@ abstract class Import
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::debug(sprintf('%s:Import Commited',self::LOGKEY));
|
Log::debug(sprintf('%s:= Import Commited',self::LOGKEY));
|
||||||
|
|
||||||
return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);
|
return collect([
|
||||||
|
'dn'=>$o->getDN(),
|
||||||
|
'link'=>$o->getDNSecure(),
|
||||||
|
'result'=>$action === self::LDAP_IMPORT_ADD ? __('Created') : __('Modified'),
|
||||||
|
]);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new GeneralException('Unhandled action during commit: '.$action);
|
throw new GeneralException('Unhandled action during commit: '.$action);
|
||||||
|
@ -24,7 +24,9 @@ class LDIF extends Import
|
|||||||
public function process(): Collection
|
public function process(): Collection
|
||||||
{
|
{
|
||||||
$c = 0;
|
$c = 0;
|
||||||
$action = NULL;
|
$action = self::LDAP_IMPORT_ADD; // Assume add mode
|
||||||
|
$subaction = 'add'; // Assume add
|
||||||
|
$dn = NULL;
|
||||||
$attribute = NULL;
|
$attribute = NULL;
|
||||||
$base64encoded = FALSE;
|
$base64encoded = FALSE;
|
||||||
$o = NULL;
|
$o = NULL;
|
||||||
@ -32,21 +34,29 @@ class LDIF extends Import
|
|||||||
$version = NULL;
|
$version = NULL;
|
||||||
$result = collect();
|
$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) {
|
foreach (preg_split('/(\r?\n|\r)/',$this->input) as $line) {
|
||||||
$c++;
|
$c++;
|
||||||
Log::debug(sprintf('%s:LDIF Line [%s]',self::LOGKEY,$line));
|
Log::debug(sprintf('%s:LDIF Line [%s] (%d)',self::LOGKEY,$line,$c));
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
|
|
||||||
// If the line starts with a comment, ignore it
|
// If the line starts with a comment, ignore it
|
||||||
if (preg_match('/^#/',$line))
|
if (preg_match('/^#/',$line))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// If the line starts with a dash, its more updates to the same entry
|
||||||
|
if (preg_match('/^-/',$line)) {
|
||||||
|
$o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
|
||||||
|
|
||||||
|
$base64encoded = FALSE;
|
||||||
|
$value = '';
|
||||||
|
$attribute = NULL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a blank line, then that completes this command
|
// If we have a blank line, then that completes this command
|
||||||
if (! $line) {
|
if (! $line) {
|
||||||
if (! is_null($o)) {
|
if (! is_null($o)) {
|
||||||
// Add the last attribute;
|
$o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
|
||||||
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
|
|
||||||
|
|
||||||
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
|
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
|
||||||
|
|
||||||
@ -59,27 +69,106 @@ class LDIF extends Import
|
|||||||
$base64encoded = FALSE;
|
$base64encoded = FALSE;
|
||||||
$attribute = NULL;
|
$attribute = NULL;
|
||||||
$value = '';
|
$value = '';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new GeneralException(sprintf('Processing Error - Line exists [%s] on (%d) but object is NULL',$line,$c));
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$m = [];
|
$m = [];
|
||||||
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
|
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s*(.*)$/',$line,$m);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$base64encoded = ($m[2] === '::');
|
||||||
|
$value = $m[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// changetype needs to be after the dn, and if it isnt we'll assume add.
|
||||||
|
if ($dn && Arr::get($m,1) !== 'changetype') {
|
||||||
|
if ($action === self::LDAP_IMPORT_ADD) {
|
||||||
|
Log::debug(sprintf('%s:Creating new entry [%s]:',self::LOGKEY,$dn),['o'=>$o]);
|
||||||
|
$o = new Entry;
|
||||||
|
$o->setDn($dn);
|
||||||
|
$dn = NULL;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log::debug(sprintf('%s:Looking for existing entry [%s]:',self::LOGKEY,$dn),['o'=>$o]);
|
||||||
|
$o = Entry::find($dn);
|
||||||
|
$dn = NULL;
|
||||||
|
|
||||||
|
if (! $o) {
|
||||||
|
$result->push(collect(['dn'=>$dn,'result'=>__('Entry doesnt exist')]));
|
||||||
|
$result->last()->put('line',$c);;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (Arr::get($m,1)) {
|
switch (Arr::get($m,1)) {
|
||||||
|
case 'dn':
|
||||||
|
$dn = $base64encoded ? base64_decode($value) : $value;
|
||||||
|
Log::debug(sprintf('%s:Got DN [%s]:',self::LOGKEY,$dn));
|
||||||
|
|
||||||
|
$value = '';
|
||||||
|
$base64encoded = FALSE;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'changetype':
|
case 'changetype':
|
||||||
if ($m[2] !== ':')
|
if ($m[2] !== ':')
|
||||||
throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
|
throw new GeneralException(sprintf('changetype cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
|
||||||
|
|
||||||
|
if (! is_null($o))
|
||||||
|
throw new GeneralException(sprintf('Previous Entry not complete? (line %d)',$c));
|
||||||
|
|
||||||
|
Log::debug(sprintf('%s:- Action [%s]',self::LOGKEY,$m[3]));
|
||||||
|
|
||||||
switch ($m[3]) {
|
switch ($m[3]) {
|
||||||
// if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) {
|
case 'add':
|
||||||
|
$action = self::LDAP_IMPORT_ADD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'modify':
|
||||||
|
$action = self::LDAP_IMPORT_MODIFY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/*
|
||||||
|
case 'delete':
|
||||||
|
$action = self::LDAP_IMPORT_DELETE;
|
||||||
|
break;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @todo modrdn|moddn
|
||||||
default:
|
default:
|
||||||
throw new NotImplementedException(sprintf('Unknown change type [%s]? (line %d)',$m[3],$c));
|
throw new NotImplementedException(sprintf('Unknown change type [%s]? (line %d)',$m[3],$c));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'add':
|
||||||
|
case 'replace':
|
||||||
|
if ($action !== self::LDAP_IMPORT_MODIFY)
|
||||||
|
throw new GeneralException(sprintf('%s action can only be used with changetype: modify (line %d)',$m[1],$c));
|
||||||
|
|
||||||
|
$subaction = $m[1];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$subaction = $m[1];
|
||||||
|
$attribute = NULL;
|
||||||
|
$o = $this->entry($o,$m[3],'delete','',$c);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'version':
|
case 'version':
|
||||||
if (! is_null($version))
|
if (! is_null($version))
|
||||||
throw new VersionException(sprintf('Version has already been set at [%d]. (line %d)',$version,$c));
|
throw new VersionException(sprintf('Version has already been set at [%d]. (line %d)',$version,$c));
|
||||||
@ -92,49 +181,12 @@ class LDIF extends Import
|
|||||||
|
|
||||||
// Treat it as an attribute
|
// Treat it as an attribute
|
||||||
default:
|
default:
|
||||||
// If $m is NULL, then this is the 2nd (or more) line of a base64 encoded value
|
// Start of a new attribute
|
||||||
if (! $m) {
|
$attribute = $m[1];
|
||||||
$value .= $line;
|
Log::debug(sprintf('%s:- Working with Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
|
||||||
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
|
// We are ready to create the entry or add the attribute
|
||||||
if ($attribute) {
|
$o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
|
||||||
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->addAttributeItem($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] === '::');
|
|
||||||
$attribute = $m[1];
|
|
||||||
$value = $m[3];
|
|
||||||
|
|
||||||
Log::debug(sprintf('%s:- New Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($version !== 1)
|
if ($version !== 1)
|
||||||
@ -142,9 +194,9 @@ class LDIF extends Import
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We may still have a pending action
|
// We may still have a pending action
|
||||||
if ($action) {
|
if ($o) {
|
||||||
// Add the last attribute;
|
if ($attribute)
|
||||||
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
|
$o = $this->entry($o,$attribute,$subaction,$base64encoded ? base64_decode($value) : $value,$c);
|
||||||
|
|
||||||
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
|
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
|
||||||
|
|
||||||
@ -156,75 +208,40 @@ class LDIF extends Import
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function xreadEntry() {
|
private function entry(Entry $o,string $attribute,string $subaction,string $value,int $c): Entry
|
||||||
static $haveVersion = FALSE;
|
{
|
||||||
|
Log::debug(sprintf('%s:/ %s Attribute [%s] value [%s] (%d)',self::LOGKEY,$subaction,$attribute,$value,$c));
|
||||||
|
|
||||||
if ($lines = $this->nextLines()) {
|
switch ($subaction) {
|
||||||
|
case 'add':
|
||||||
|
if (! $value)
|
||||||
|
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
|
||||||
|
|
||||||
$server = $this->getServer();
|
$o->addAttributeItem($attribute,$value);
|
||||||
|
break;
|
||||||
|
|
||||||
# The first line should be the DN
|
case 'replace':
|
||||||
if (preg_match('/^dn:/',$lines[0])) {
|
if (! $value)
|
||||||
list($text,$dn) = $this->getAttrValue(array_shift($lines));
|
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
|
||||||
|
|
||||||
# The second line should be our changetype
|
if (! $o->getObject($attribute))
|
||||||
if (preg_match('/^changetype:[ ]*(delete|add|modrdn|moddn|modify)/i',$lines[0])) {
|
throw new \Exception(sprintf('Attribute [%s] doesnt exist in [%s] (line %d)',$attribute,$o->getDn(),$c));
|
||||||
$attrvalue = $this->getAttrValue($lines[0]);
|
|
||||||
$changetype = $attrvalue[1];
|
|
||||||
array_shift($lines);
|
|
||||||
|
|
||||||
} else
|
$o->{$attribute} = [Entry::TAG_NOTAG=>[$value]];
|
||||||
$changetype = 'add';
|
|
||||||
|
|
||||||
$this->template = new Template($this->server_id,NULL,NULL,$changetype);
|
break;
|
||||||
|
|
||||||
switch ($changetype) {
|
case 'delete':
|
||||||
case 'add':
|
if (! $o->getObject($attribute))
|
||||||
$rdn = get_rdn($dn);
|
throw new \Exception(sprintf('Attribute [%s] doesnt exist in [%s] (line %d)',$attribute,$o->getDn(),$c));
|
||||||
$container = $server->getContainer($dn);
|
|
||||||
|
|
||||||
$this->template->setContainer($container);
|
$o->{$attribute} = [];
|
||||||
$this->template->accept();
|
break;
|
||||||
|
|
||||||
$this->getAddDetails($lines);
|
default:
|
||||||
$this->template->setRDNAttributes($rdn);
|
throw new \Exception('Unknown subaction:'.$subaction);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->template;
|
return $o;
|
||||||
|
|
||||||
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(_('Unknown change type'),$lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else
|
|
||||||
return $this->error(_('A valid dn line is required'),$lines);
|
|
||||||
|
|
||||||
} else
|
|
||||||
return FALSE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -101,6 +101,30 @@ class Entry extends Model
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This replaces the model's get dirty, given that we store LDAP attributes in $this->objects, replacing
|
||||||
|
* $this->original/$this->attributes
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDirty(): array
|
||||||
|
{
|
||||||
|
$result = collect();
|
||||||
|
|
||||||
|
foreach ($this->objects as $o)
|
||||||
|
if ($o->isDirty())
|
||||||
|
$result = $result->merge($o->getDirty());
|
||||||
|
|
||||||
|
$result = $result
|
||||||
|
->flatMap(function($item,$attr) {
|
||||||
|
return ($x=collect($item))->count()
|
||||||
|
? $x->flatMap(fn($v,$k)=>[strtolower($attr.(($k !== self::TAG_NOTAG) ? ';'.$k : ''))=>$v])
|
||||||
|
: [strtolower($attr)=>[]];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $result->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the new and old values for a given key are equivalent.
|
* Determine if the new and old values for a given key are equivalent.
|
||||||
*/
|
*/
|
||||||
@ -454,7 +478,7 @@ class Entry extends Model
|
|||||||
$ot = $this->getOtherTags();
|
$ot = $this->getOtherTags();
|
||||||
|
|
||||||
$cache[$tag ?: '_all_'] = $this->objects
|
$cache[$tag ?: '_all_'] = $this->objects
|
||||||
->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === Entry::TAG_NOTAG)))
|
->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === self::TAG_NOTAG)))
|
||||||
->filter(fn($item)=>(! $tag) || $ot->has($item->name_lc) || count($item->tagValues($tag)) > 0);
|
->filter(fn($item)=>(! $tag) || $ot->has($item->name_lc) || count($item->tagValues($tag)) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,13 @@
|
|||||||
</thead>
|
</thead>
|
||||||
@foreach($result as $item)
|
@foreach($result as $item)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $item->get('dn') }}</td>
|
<td>
|
||||||
|
@if($x=$item->get('link'))
|
||||||
|
<a href="{{ url('/') }}#{{ $x }}">{{ $item->get('dn') }}</a>
|
||||||
|
@else
|
||||||
|
{{ $item->get('dn') }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
<td>{{ $item->get('result') }}</td>
|
<td>{{ $item->get('result') }}</td>
|
||||||
<td class="text-end">{{ $item->get('line') }}</td>
|
<td class="text-end">{{ $item->get('line') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user