Added Schema Retreival
This commit is contained in:
parent
376dedbcab
commit
732ea8b978
@ -116,29 +116,8 @@ abstract class Kohana_Database_LDAP extends Kohana_LDAP {
|
||||
}
|
||||
|
||||
if ($this->_instance == 'auth') {
|
||||
$x = LDAP::factory('schema',NULL,$this->_config);
|
||||
$this->getSchema();
|
||||
|
||||
try {
|
||||
// Our Auth Bind credentials are wrong
|
||||
if ($x->bind((isset($this->_config['schema']['dn']) ? $this->_config['schema']['dn'] : 'fred'),(isset($this->_config['schema']['password']) ? $this->_config['schema']['password'] : 'fred'))) {
|
||||
|
||||
$u = $x->search(array(''))
|
||||
->scope('base')
|
||||
->where('objectclass','=','*')
|
||||
->execute();
|
||||
|
||||
if (! $u OR ! isset($u[''][0]['subschemasubentry'][0]))
|
||||
throw new Kohana_Exception('Couldnt find schema?');
|
||||
|
||||
$x->setSchema(ORM::factory('LDAP_Schema',$u[''][0]['subschemasubentry'][0]));
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// If we are a command line, we can just print the error
|
||||
echo _('Unable to retrieve the SCHEMA from the LDAP server.');
|
||||
echo _('The error message is').': '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($benchmark))
|
||||
@ -305,6 +284,34 @@ abstract class Kohana_Database_LDAP extends Kohana_LDAP {
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function getSchema() {
|
||||
// Make sure our login_attr is DN
|
||||
if ($this->_instance == 'schema' AND $this->_config['login_attr'] != 'DN')
|
||||
$this->_config['login_attr'] = 'DN';
|
||||
|
||||
$x = LDAP::factory('schema');
|
||||
|
||||
try {
|
||||
// @todo We should bind as specific shema DN, logged in User or anonymous.
|
||||
if ($x->bind((isset($this->_config['schema']['dn']) ? $this->_config['schema']['dn'] : FALSE),(isset($this->_config['schema']['password']) ? $this->_config['schema']['password'] : FALSE))) {
|
||||
$u = $x->search(array(''))
|
||||
->scope('base')
|
||||
->execute();
|
||||
|
||||
if (! $u OR ! isset($u[''][0]['subschemasubentry'][0]))
|
||||
throw new Kohana_Exception('Couldnt find schema?');
|
||||
|
||||
$x->setSchema(ORM::factory('LDAP_Schema',$u[''][0]['subschemasubentry'][0]));
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// If we are a command line, we can just print the error
|
||||
echo _('Unable to retrieve the SCHEMA from the LDAP server.');
|
||||
echo _('The error message is').': '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
public function search($base=array()) {
|
||||
return new Database_LDAP_Search($this,$base);
|
||||
}
|
||||
|
@ -212,7 +212,8 @@ abstract class Kohana_Database_LDAP_Search_Builder_Query extends Database_Query_
|
||||
return '(|'.$filter.')';
|
||||
|
||||
case '':
|
||||
return '(&(objectClass=*))';
|
||||
return 'objectClass=*';
|
||||
|
||||
default:
|
||||
throw new Kohana_Exception('Condition ":condition" not handled.',array(':condition'=>$current_condition));
|
||||
}
|
||||
|
@ -107,6 +107,9 @@ abstract class Kohana_LDAP extends Database {
|
||||
}
|
||||
|
||||
public function schema() {
|
||||
if (is_null($this->_schema))
|
||||
$this->getSchema();
|
||||
|
||||
return $this->_schema;
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,12 @@ abstract class Kohana_ORM_LDAP extends ORM {
|
||||
}
|
||||
}
|
||||
|
||||
public function __get($column) {
|
||||
$x = parent::__get($column);
|
||||
|
||||
return (! is_object($x) AND is_object(LDAP::factory('schema')->schema()->attribute($column)) AND LDAP::factory('schema')->schema()->attribute($column)->getIsSingleValue()) ? $x[0] : $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Database Builder to given query type
|
||||
*
|
||||
|
66
classes/Kohana/Schema.php
Normal file
66
classes/Kohana/Schema.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class holds our LDAP Schema
|
||||
*
|
||||
* @package LDAP
|
||||
* @category Schema
|
||||
* @author Deon George
|
||||
* @copyright (c) 2013 phpLDAPadmin Development Team
|
||||
* @license http://dev.phpldapadmin.org/license.html
|
||||
*/
|
||||
abstract class Kohana_Schema {
|
||||
// The schema item's name.
|
||||
protected $name = null;
|
||||
// The OID of this schema item.
|
||||
private $oid = null;
|
||||
// The description of this schema item.
|
||||
protected $description = null;
|
||||
// Boolean value indicating whether this objectClass is obsolete
|
||||
private $is_obsolete = false;
|
||||
// Seconds to cache our schema details for
|
||||
protected $_cache_time = 86400;
|
||||
|
||||
public function getDescription() {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this objectClass is flagged as obsolete by the LDAP server.
|
||||
*/
|
||||
public function getIsObsolete() {
|
||||
return $this->is_obsolete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the objects name.
|
||||
*
|
||||
* param boolean $lower Return the name in lower case (default)
|
||||
* @return string The name
|
||||
*/
|
||||
public function getName($lower=true) {
|
||||
return $lower ? strtolower($this->name) : $this->name;
|
||||
}
|
||||
|
||||
public function getOID() {
|
||||
return $this->oid;
|
||||
}
|
||||
|
||||
public function setDescription($desc) {
|
||||
$this->description = $desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attriute's name.
|
||||
*
|
||||
* @param string $name The new name to give this attribute.
|
||||
*/
|
||||
public function setName($name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setOID($oid) {
|
||||
$this->oid = $oid;
|
||||
}
|
||||
}
|
||||
?>
|
464
classes/Kohana/Schema/Attribute.php
Normal file
464
classes/Kohana/Schema/Attribute.php
Normal file
@ -0,0 +1,464 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class holds our LDAP Schema
|
||||
*
|
||||
* @package LDAP
|
||||
* @category Schema
|
||||
* @author Deon George
|
||||
* @copyright (c) 2013 phpLDAPadmin Development Team
|
||||
* @license http://dev.phpldapadmin.org/license.html
|
||||
*/
|
||||
abstract class Kohana_Schema_Attribute extends Schema {
|
||||
// An array of alias attribute names, strings
|
||||
private $aliases = array();
|
||||
// The equality rule used
|
||||
private $equality = null;
|
||||
// This attribute has been forced a MAY attribute by the configuration.
|
||||
private $forced_as_may = false;
|
||||
// boolean: is collective?
|
||||
private $is_collective = false;
|
||||
// boolean: can use modify?
|
||||
private $is_no_user_modification = false;
|
||||
// boolean: is single valued only?
|
||||
private $is_single_value = false;
|
||||
// The max number of characters this attribute can be
|
||||
private $max_length = null;
|
||||
// The ordering of the attributeType
|
||||
private $ordering = null;
|
||||
// A list of object class names that require this attribute type.
|
||||
private $required_by_object_classes = array();
|
||||
// Boolean: supports substring matching?
|
||||
private $sub_str = null;
|
||||
// The attribute from which this attribute inherits (if any)
|
||||
private $sup_attribute = null;
|
||||
// The full syntax string, ie 1.2.3.4{16}
|
||||
private $syntax = null;
|
||||
private $syntax_oid = null;
|
||||
// A string description of the syntax type (taken from the LDAPSyntaxes)
|
||||
private $type = null;
|
||||
// The usage string set by the LDAP schema
|
||||
private $usage = null;
|
||||
// An array of objectClasses which use this attributeType (must be set by caller)
|
||||
private $used_in_object_classes = array();
|
||||
|
||||
public function __construct($line) {
|
||||
if (! $line)
|
||||
return $this;
|
||||
|
||||
if ($x = Cache::instance()->get($line))
|
||||
return $x;
|
||||
|
||||
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
for ($i=0; $i<count($strings); $i++) {
|
||||
switch ($strings[$i]) {
|
||||
case '(':
|
||||
break;
|
||||
|
||||
case 'NAME':
|
||||
# Some schema's return a (' instead of a ( '
|
||||
if ($strings[$i+1] != '(' && ! preg_match('/^\(/',$strings[$i+1])) {
|
||||
do {
|
||||
$i++;
|
||||
if (strlen($this->name)==0)
|
||||
$this->name = $strings[$i];
|
||||
else
|
||||
$this->name .= ' '.$strings[$i];
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
|
||||
# This attribute has no aliases
|
||||
$this->aliases = array();
|
||||
|
||||
} else {
|
||||
$i++;
|
||||
do {
|
||||
# In case we came here becaues of a ('
|
||||
if (preg_match('/^\(/',$strings[$i]))
|
||||
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
|
||||
else
|
||||
$i++;
|
||||
|
||||
if (strlen($this->name) == 0)
|
||||
$this->name = $strings[$i];
|
||||
else
|
||||
$this->name .= ' '.$strings[$i];
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
|
||||
# Add alias names for this attribute
|
||||
while ($strings[++$i] != ')') {
|
||||
$alias = $strings[$i];
|
||||
$alias = preg_replace("/^\'/",'',$alias);
|
||||
$alias = preg_replace("/\'$/",'',$alias);
|
||||
$this->addAlias($alias);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'DESC':
|
||||
do {
|
||||
$i++;
|
||||
if (strlen($this->description)==0)
|
||||
$this->description=$this->description.$strings[$i];
|
||||
else
|
||||
$this->description=$this->description.' '.$strings[$i];
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
|
||||
break;
|
||||
|
||||
case 'OBSOLETE':
|
||||
$this->is_obsolete = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
case 'SUP':
|
||||
$i++;
|
||||
$this->sup_attribute = $strings[$i];
|
||||
|
||||
break;
|
||||
|
||||
case 'EQUALITY':
|
||||
$i++;
|
||||
$this->equality = $strings[$i];
|
||||
|
||||
break;
|
||||
|
||||
case 'ORDERING':
|
||||
$i++;
|
||||
$this->ordering = $strings[$i];
|
||||
|
||||
break;
|
||||
|
||||
case 'SUBSTR':
|
||||
$i++;
|
||||
$this->sub_str = $strings[$i];
|
||||
|
||||
break;
|
||||
|
||||
case 'SYNTAX':
|
||||
$i++;
|
||||
$this->syntax = $strings[$i];
|
||||
$this->syntax_oid = preg_replace('/{\d+}$/','',$this->syntax);
|
||||
|
||||
# Does this SYNTAX string specify a max length (ie, 1.2.3.4{16})
|
||||
if (preg_match('/{(\d+)}$/',$this->syntax,$this->max_length))
|
||||
$this->max_length = $this->max_length[1];
|
||||
else
|
||||
$this->max_length = null;
|
||||
|
||||
if ($i < count($strings) - 1 && $strings[$i+1] == '{') {
|
||||
do {
|
||||
$i++;
|
||||
$this->name .= ' '.$strings[$i];
|
||||
} while ($strings[$i] != '}');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'SINGLE-VALUE':
|
||||
$this->is_single_value = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
case 'COLLECTIVE':
|
||||
$this->is_collective = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
case 'NO-USER-MODIFICATION':
|
||||
$this->is_no_user_modification = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
case 'USAGE':
|
||||
$i++;
|
||||
$this->usage = $strings[$i];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if (preg_match('/[\d\.]+/i',$strings[$i]) && $i == 1)
|
||||
$this->oid = $strings[$i];
|
||||
}
|
||||
}
|
||||
|
||||
$this->name = preg_replace("/^\'/",'',$this->name);
|
||||
$this->name = preg_replace("/\'$/",'',$this->name);
|
||||
$this->description = preg_replace("/^\'/",'',$this->description);
|
||||
$this->description = preg_replace("/\'$/",'',$this->description);
|
||||
$this->syntax = preg_replace("/^\'/",'',$this->syntax);
|
||||
$this->syntax = preg_replace("/\'$/",'',$this->syntax);
|
||||
$this->syntax_oid = preg_replace("/^\'/",'',$this->syntax_oid);
|
||||
$this->syntax_oid = preg_replace("/\'$/",'',$this->syntax_oid);
|
||||
$this->sup_attribute = preg_replace("/^\'/",'',$this->sup_attribute);
|
||||
$this->sup_attribute = preg_replace("/\'$/",'',$this->sup_attribute);
|
||||
|
||||
Cache::instance()->set($line,$this,$this->_cache_time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attribute name to the alias array.
|
||||
*
|
||||
* @param string $alias The name of a new attribute to add to this attribute's list of aliases.
|
||||
*/
|
||||
public function addAlias($alias) {
|
||||
array_push($this->aliases,$alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an objectClass name to this attribute's list of "required by" objectClasses,
|
||||
* that is the list of objectClasses which must have this attribute.
|
||||
*
|
||||
* @param string $name The name of the objectClass to add.
|
||||
*/
|
||||
public function addRequiredByObjectClass($name) {
|
||||
foreach ($this->required_by_object_classes as $required_by_object_class)
|
||||
if (strcasecmp($required_by_object_class,$name) == 0)
|
||||
return false;
|
||||
|
||||
array_push($this->required_by_object_classes,$name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an objectClass name to this attribute's list of "used in" objectClasses,
|
||||
* that is the list of objectClasses which provide this attribute.
|
||||
*
|
||||
* @param string $name The name of the objectClass to add.
|
||||
*/
|
||||
public function addUsedInObjectClass($name) {
|
||||
foreach ($this->used_in_object_classes as $used_in_object_class) {
|
||||
if (strcasecmp($used_in_object_class,$name) == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
array_push($this->used_in_object_classes,$name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of attributes that are an alias for this attribute (if any).
|
||||
*
|
||||
* @return array An array of names of attributes which alias this attribute or
|
||||
* an empty array if no attribute aliases this object.
|
||||
*/
|
||||
public function getAliases() {
|
||||
return $this->aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's equality string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquality() {
|
||||
return $this->equality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this attribute is collective.
|
||||
*
|
||||
* @return boolean Returns true if this attribute is collective and false otherwise.
|
||||
*/
|
||||
public function getIsCollective() {
|
||||
return $this->is_collective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this attribute is not modifiable by users.
|
||||
*
|
||||
* @return boolean Returns true if this attribute is not modifiable by users.
|
||||
*/
|
||||
public function getIsNoUserModification() {
|
||||
return $this->is_no_user_modification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this attribute is single-valued. If this attribute only supports single values, true
|
||||
* is returned. If this attribute supports multiple values, false is returned.
|
||||
*
|
||||
* @return boolean Returns true if this attribute is single-valued or false otherwise.
|
||||
*/
|
||||
public function getIsSingleValue() {
|
||||
return $this->is_single_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's the maximum length. If no maximum is defined by the LDAP server, null is returned.
|
||||
*
|
||||
* @return int The maximum length (in characters) of this attribute or null if no maximum is specified.
|
||||
*/
|
||||
public function getMaxLength() {
|
||||
return $this->max_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's ordering specification.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOrdering() {
|
||||
return $this->ordering;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of "required by" objectClasses, that is the list of objectClasses
|
||||
* which provide must have attribute.
|
||||
*
|
||||
* @return array An array of names of objectclasses (strings) which provide this attribute
|
||||
*/
|
||||
public function getRequiredByObjectClasses() {
|
||||
return $this->required_by_object_classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's parent attribute (if any). If this attribute does not
|
||||
* inherit from another attribute, null is returned.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSupAttribute() {
|
||||
return $this->sup_attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's substring matching specification
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSubstr() {
|
||||
return $this->sub_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's syntax OID. Differs from getSyntaxString() in that this
|
||||
* function only returns the actual OID with any length specification removed.
|
||||
* Ie, if the syntax string is "1.2.3.4{16}", this function only retruns
|
||||
* "1.2.3.4".
|
||||
*
|
||||
* @return string The syntax OID string.
|
||||
*/
|
||||
public function getSyntaxOID() {
|
||||
return $this->syntax_oid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's raw syntax string (ie: "1.2.3.4{16}").
|
||||
*
|
||||
* @return string The raw syntax string
|
||||
*/
|
||||
public function getSyntaxString() {
|
||||
return $this->syntax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's type
|
||||
*
|
||||
* @return string The attribute's type.
|
||||
*/
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's usage string as defined by the LDAP server
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUsage() {
|
||||
return $this->usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of "used in" objectClasses, that is the list of objectClasses
|
||||
* which provide this attribute.
|
||||
*
|
||||
* @return array An array of names of objectclasses (strings) which provide this attribute
|
||||
*/
|
||||
public function getUsedInObjectClasses() {
|
||||
return $this->used_in_object_classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified attribute is an alias for this one (based on this attribute's alias list).
|
||||
*
|
||||
* @param string $attr_name The name of the attribute to check.
|
||||
* @return boolean True if the specified attribute is an alias for this one, or false otherwise.
|
||||
*/
|
||||
public function isAliasFor($attr_name) {
|
||||
foreach ($this->aliases as $alias_attr_name)
|
||||
if (strcasecmp($alias_attr_name,$attr_name) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isForceMay() {
|
||||
return $this->forced_as_may;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attribute name from this attribute's alias array.
|
||||
*
|
||||
* @param string $remove_alias_name The name of the attribute to remove.
|
||||
* @return boolean true on success or false on failure (ie, if the specified
|
||||
* attribute name is not found in this attribute's list of aliases)
|
||||
*/
|
||||
public function removeAlias($remove_alias_name) {
|
||||
foreach ($this->aliases as $i => $alias_name) {
|
||||
|
||||
if (strcasecmp($alias_name,$remove_alias_name) == 0) {
|
||||
unset($this->aliases[$i]);
|
||||
|
||||
$this->aliases = array_values($this->aliases);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attribute's list of aliases.
|
||||
*
|
||||
* @param array $aliases The array of alias names (strings)
|
||||
*/
|
||||
public function setAliases($aliases) {
|
||||
$this->aliases = $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will mark this attribute as a forced MAY attribute
|
||||
*/
|
||||
public function setForceMay() {
|
||||
$this->forced_as_may = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this attribute is single-valued.
|
||||
*
|
||||
* @param boolean $is
|
||||
*/
|
||||
public function setIsSingleValue($is) {
|
||||
$this->is_single_value = $is;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attriute's SUP attribute (ie, the attribute from which this attribute inherits).
|
||||
*
|
||||
* @param string $attr The name of the new parent (SUP) attribute
|
||||
*/
|
||||
public function setSupAttribute($attr) {
|
||||
$this->sup_attribute = $attr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attribute's type.
|
||||
*
|
||||
* @param string $type The new type.
|
||||
*/
|
||||
public function setType($type) {
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
||||
?>
|
@ -10,5 +10,16 @@
|
||||
* @license http://dev.phpldapadmin.org/license.html
|
||||
*/
|
||||
class Model_LDAP_Schema extends ORM_LDAP {
|
||||
public function attribute($column) {
|
||||
$k = 'attributetypes';
|
||||
|
||||
return new Schema_Attribute(($x = $this->_attr_search($k,$column) ? $this->_object[$k][$x] : '');
|
||||
}
|
||||
|
||||
private function _attr_search($key,$column) {
|
||||
foreach ($this->_object[$key] as $k=>$v)
|
||||
if (preg_match("/^\( (.*) NAME (\( )?\'$column\' /i",$v))
|
||||
return $k;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
4
classes/Schema.php
Normal file
4
classes/Schema.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
abstract class Schema extends Kohana_Schema {}
|
||||
?>
|
4
classes/Schema/Attribute.php
Normal file
4
classes/Schema/Attribute.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
class Schema_Attribute extends Kohana_Schema_Attribute {}
|
||||
?>
|
Reference in New Issue
Block a user