Merge pull request #191 from MarBie77/master
This commit is contained in:
19
README.md
19
README.md
@@ -1,5 +1,8 @@
|
|||||||
External user authentication
|
External user authentication
|
||||||
============================
|
============================
|
||||||
|
|
||||||
|
**⚠⚠ Warning:** As of Version 3.0 this app uses namespace \OCA\UserExternal now. You MUST change your config to adopt to this change. See examples below. ⚠⚠
|
||||||
|
|
||||||
**Authenticate user login against IMAP, SMB, FTP, WebDAV, HTTP BasicAuth, SSH and XMPP**
|
**Authenticate user login against IMAP, SMB, FTP, WebDAV, HTTP BasicAuth, SSH and XMPP**
|
||||||
|
|
||||||
Passwords are not stored locally; authentication always happens against
|
Passwords are not stored locally; authentication always happens against
|
||||||
@@ -30,7 +33,7 @@ Add the following to `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_FTP',
|
'class' => '\OCA\UserExternal\FTP',
|
||||||
'arguments' => array('127.0.0.1'),
|
'arguments' => array('127.0.0.1'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -39,7 +42,7 @@ To enable SSL connections via `ftps`, append a second parameter `true`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_FTP',
|
'class' => '\OCA\UserExternal\FTP',
|
||||||
'arguments' => array('127.0.0.1', true),
|
'arguments' => array('127.0.0.1', true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -68,7 +71,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_IMAP',
|
'class' => '\OCA\UserExternal\IMAP',
|
||||||
'arguments' => array(
|
'arguments' => array(
|
||||||
'127.0.0.1', 993, 'ssl', 'example.com', true, false
|
'127.0.0.1', 993, 'ssl', 'example.com', true, false
|
||||||
),
|
),
|
||||||
@@ -104,7 +107,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_SMB',
|
'class' => '\OCA\UserExternal\SMB',
|
||||||
'arguments' => array('127.0.0.1'),
|
'arguments' => array('127.0.0.1'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -128,7 +131,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => '\OCA\User_External\WebDAVAuth',
|
'class' => '\OCA\UserExternal\WebDAVAuth',
|
||||||
'arguments' => array('https://example.com/webdav'),
|
'arguments' => array('https://example.com/webdav'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -151,7 +154,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_BasicAuth',
|
'class' => '\OCA\UserExternal\BasicAuth',
|
||||||
'arguments' => array('https://example.com/basic_auth'),
|
'arguments' => array('https://example.com/basic_auth'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -172,7 +175,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class' => 'OC_User_SSH',
|
'class' => '\OCA\UserExternal\SSH',
|
||||||
'arguments' => array('127.0.0.1', '22'),
|
'arguments' => array('127.0.0.1', '22'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -193,7 +196,7 @@ Add the following to your `config.php`:
|
|||||||
|
|
||||||
'user_backends' => array (
|
'user_backends' => array (
|
||||||
0 => array (
|
0 => array (
|
||||||
'class' => 'OC_User_XMPP',
|
'class' => '\OCA\UserExternal\XMPP',
|
||||||
'arguments' => array (
|
'arguments' => array (
|
||||||
0 => 'dbhost',
|
0 => 'dbhost',
|
||||||
1 => 'prosodydb',
|
1 => 'prosodydb',
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
OC::$CLASSPATH['OC_User_IMAP']='user_external/lib/imap.php';
|
|
||||||
OC::$CLASSPATH['OC_User_SMB']='user_external/lib/smb.php';
|
|
||||||
OC::$CLASSPATH['OC_User_FTP']='user_external/lib/ftp.php';
|
|
||||||
OC::$CLASSPATH['OC_User_BasicAuth']='user_external/lib/basicauth.php';
|
|
||||||
OC::$CLASSPATH['OC_User_SSH']='user_external/lib/ssh.php';
|
|
||||||
OC::$CLASSPATH['OC_User_XMPP']='user_external/lib/xmpp.php';
|
|
||||||
@@ -16,9 +16,10 @@
|
|||||||
|
|
||||||
Read the [documentation](https://github.com/nextcloud/user_external#readme) to learn how to configure it!
|
Read the [documentation](https://github.com/nextcloud/user_external#readme) to learn how to configure it!
|
||||||
]]></description>
|
]]></description>
|
||||||
<version>2.1.0</version>
|
<version>3.0.0</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Robin Appelman</author>
|
<author>Robin Appelman</author>
|
||||||
|
<namespace>UserExternal</namespace>
|
||||||
<types>
|
<types>
|
||||||
<prelogin/>
|
<prelogin/>
|
||||||
<authentication/>
|
<authentication/>
|
||||||
@@ -32,6 +33,6 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l
|
|||||||
<bugs>https://github.com/nextcloud/user_external/issues</bugs>
|
<bugs>https://github.com/nextcloud/user_external/issues</bugs>
|
||||||
<repository type="git">https://github.com/nextcloud/user_external.git</repository>
|
<repository type="git">https://github.com/nextcloud/user_external.git</repository>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<nextcloud min-version="21" max-version="22" />
|
<nextcloud min-version="22" max-version="24" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</info>
|
</info>
|
||||||
|
|||||||
26
lib/AppInfo/Application.php
Normal file
26
lib/AppInfo/Application.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\UserExternal\AppInfo;
|
||||||
|
|
||||||
|
use OCP\AppFramework\App;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
|
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||||
|
use OCP\Notification\IManager;
|
||||||
|
use OCP\User\Events;
|
||||||
|
|
||||||
|
class Application extends App implements IBootstrap {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct('user_external');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(IRegistrationContext $context): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(IBootContext $context): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* later.
|
* later.
|
||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
namespace OCA\user_external;
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for external auth implementations that stores users
|
* Base class for external auth implementations that stores users
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class OC_User_BasicAuth extends \OCA\user_external\Base {
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
|
class BasicAuth extends Base {
|
||||||
|
|
||||||
private $authUrl;
|
private $authUrl;
|
||||||
|
|
||||||
@@ -36,14 +38,14 @@ class OC_User_BasicAuth extends \OCA\user_external\Base {
|
|||||||
);
|
);
|
||||||
$canary = get_headers($this->authUrl, 1, $context);
|
$canary = get_headers($this->authUrl, 1, $context);
|
||||||
if(!$canary) {
|
if(!$canary) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
|
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isset(array_change_key_case($canary, CASE_LOWER)['www-authenticate'])) {
|
if (!isset(array_change_key_case($canary, CASE_LOWER)['www-authenticate'])) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Mis-configured BasicAuth Url: '.$this->authUrl.', provided URL does not do authentication!',
|
'ERROR: Mis-configured BasicAuth Url: '.$this->authUrl.', provided URL does not do authentication!',
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -60,7 +62,7 @@ class OC_User_BasicAuth extends \OCA\user_external\Base {
|
|||||||
$headers = get_headers($this->authUrl, 1, $context);
|
$headers = get_headers($this->authUrl, 1, $context);
|
||||||
|
|
||||||
if(!$headers) {
|
if(!$headers) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
|
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -81,7 +83,7 @@ class OC_User_BasicAuth extends \OCA\user_external\Base {
|
|||||||
$this->storeUser($uid);
|
$this->storeUser($uid);
|
||||||
return $uid;
|
return $uid;
|
||||||
case "3":
|
case "3":
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Too many redirects from BasicAuth Url: '.$this->authUrl,
|
'ERROR: Too many redirects from BasicAuth Url: '.$this->authUrl,
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authentication against a FTP/FTPS server
|
* User authentication against a FTP/FTPS server
|
||||||
*
|
*
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
* @license http://www.gnu.org/licenses/agpl AGPL
|
* @license http://www.gnu.org/licenses/agpl AGPL
|
||||||
* @link http://github.com/owncloud/apps
|
* @link http://github.com/owncloud/apps
|
||||||
*/
|
*/
|
||||||
class OC_User_FTP extends \OCA\user_external\Base{
|
class FTP extends Base{
|
||||||
private $host;
|
private $host;
|
||||||
private $secure;
|
private $secure;
|
||||||
private $protocol;
|
private $protocol;
|
||||||
@@ -46,7 +48,7 @@ class OC_User_FTP extends \OCA\user_external\Base{
|
|||||||
*/
|
*/
|
||||||
public function checkPassword($uid, $password) {
|
public function checkPassword($uid, $password) {
|
||||||
if (false === array_search($this->protocol, stream_get_wrappers())) {
|
if (false === array_search($this->protocol, stream_get_wrappers())) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Stream wrapper not available: ' . $this->protocol,
|
'ERROR: Stream wrapper not available: ' . $this->protocol,
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
* later.
|
* later.
|
||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authentication against an IMAP mail server
|
* User authentication against an IMAP mail server
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
* @license http://www.gnu.org/licenses/agpl AGPL
|
* @license http://www.gnu.org/licenses/agpl AGPL
|
||||||
* @link http://github.com/owncloud/apps
|
* @link http://github.com/owncloud/apps
|
||||||
*/
|
*/
|
||||||
class OC_User_IMAP extends \OCA\user_external\Base {
|
class IMAP extends Base {
|
||||||
private $mailbox;
|
private $mailbox;
|
||||||
private $port;
|
private $port;
|
||||||
private $sslmode;
|
private $sslmode;
|
||||||
@@ -70,7 +72,7 @@ class OC_User_IMAP extends \OCA\user_external\Base {
|
|||||||
$uid = $pieces[0];
|
$uid = $pieces[0];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: User has a wrong domain! Expecting: '.$this->domain,
|
'ERROR: User has a wrong domain! Expecting: '.$this->domain,
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -105,7 +107,7 @@ class OC_User_IMAP extends \OCA\user_external\Base {
|
|||||||
$this->storeUser($uid, $groups);
|
$this->storeUser($uid, $groups);
|
||||||
return $uid;
|
return $uid;
|
||||||
} else {
|
} else {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: Could not connect to imap server via curl: '.curl_error($ch),
|
'ERROR: Could not connect to imap server via curl: '.curl_error($ch),
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -23,7 +23,7 @@ declare(strict_types=1);
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace OCA\User_external\Migration;
|
namespace OCA\UserExternal\Migration;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use OCP\DB\ISchemaWrapper;
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* later.
|
* later.
|
||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authentication via samba (smbclient)
|
* User authentication via samba (smbclient)
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
* @license http://www.gnu.org/licenses/agpl AGPL
|
* @license http://www.gnu.org/licenses/agpl AGPL
|
||||||
* @link http://github.com/owncloud/apps
|
* @link http://github.com/owncloud/apps
|
||||||
*/
|
*/
|
||||||
class OC_User_SMB extends \OCA\user_external\Base{
|
class SMB extends Base{
|
||||||
private $host;
|
private $host;
|
||||||
|
|
||||||
const SMBCLIENT = 'smbclient -L';
|
const SMBCLIENT = 'smbclient -L';
|
||||||
@@ -42,7 +43,7 @@ class OC_User_SMB extends \OCA\user_external\Base{
|
|||||||
$command = self::SMBCLIENT.' '.escapeshellarg('//' . $this->host . '/dummy').' -U '.$uidEscaped.'%'.$password;
|
$command = self::SMBCLIENT.' '.escapeshellarg('//' . $this->host . '/dummy').' -U '.$uidEscaped.'%'.$password;
|
||||||
$lastline = exec($command, $output, $retval);
|
$lastline = exec($command, $output, $retval);
|
||||||
if ($retval === 127) {
|
if ($retval === 127) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: smbclient executable missing',
|
'ERROR: smbclient executable missing',
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -55,7 +56,7 @@ class OC_User_SMB extends \OCA\user_external\Base{
|
|||||||
goto login;
|
goto login;
|
||||||
} else if ($retval !== 0) {
|
} else if ($retval !== 0) {
|
||||||
//some other error
|
//some other error
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: smbclient error: ' . trim($lastline),
|
'ERROR: smbclient error: ' . trim($lastline),
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
* later.
|
* later.
|
||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authentication against a SSH server
|
* User authentication against a SSH server
|
||||||
@@ -17,9 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
class OC_User_SSH extends \OCA\user_external\Base {
|
class SSH extends Base {
|
||||||
private $host;
|
private $host;
|
||||||
private $port;
|
private $port;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new SSH authentication provider
|
* Create a new SSH authentication provider
|
||||||
@@ -43,7 +44,7 @@ class OC_User_SSH extends \OCA\user_external\Base {
|
|||||||
*/
|
*/
|
||||||
public function checkPassword($uid, $password) {
|
public function checkPassword($uid, $password) {
|
||||||
if (!extension_loaded('ssh2')) {
|
if (!extension_loaded('ssh2')) {
|
||||||
OC::$server->getLogger()->error(
|
\OC::$server->getLogger()->error(
|
||||||
'ERROR: php-ssh2 PECL module missing',
|
'ERROR: php-ssh2 PECL module missing',
|
||||||
['app' => 'user_external']
|
['app' => 'user_external']
|
||||||
);
|
);
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace OCA\user_external;
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
class WebDavAuth extends Base {
|
class WebDavAuth extends Base {
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@ class WebDavAuth extends Base {
|
|||||||
public function checkPassword($uid, $password) {
|
public function checkPassword($uid, $password) {
|
||||||
$arr = explode('://', $this->webDavAuthUrl, 2);
|
$arr = explode('://', $this->webDavAuthUrl, 2);
|
||||||
if( ! isset($arr) OR count($arr) !== 2) {
|
if( ! isset($arr) OR count($arr) !== 2) {
|
||||||
OC::$server->getLogger()->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']);
|
\OC::$server->getLogger()->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
list($protocol, $path) = $arr;
|
list($protocol, $path) = $arr;
|
||||||
$url= $protocol.'://'.urlencode($uid).':'.urlencode($password).'@'.$path;
|
$url= $protocol.'://'.urlencode($uid).':'.urlencode($password).'@'.$path;
|
||||||
$headers = get_headers($url);
|
$headers = get_headers($url);
|
||||||
if($headers === false) {
|
if($headers === false) {
|
||||||
OC::$server->getLogger()->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']);
|
\OC::$server->getLogger()->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
* later.
|
* later.
|
||||||
* See the COPYING-README file.
|
* See the COPYING-README file.
|
||||||
*/
|
*/
|
||||||
|
namespace OCA\UserExternal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authentication against a XMPP Prosody MySQL database
|
* User authentication against a XMPP Prosody MySQL database
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
* @author Sebastian Sterk https://wiuwiu.de/Imprint
|
* @author Sebastian Sterk https://wiuwiu.de/Imprint
|
||||||
* @license http://www.gnu.org/licenses/agpl AGPL
|
* @license http://www.gnu.org/licenses/agpl AGPL
|
||||||
*/
|
*/
|
||||||
class OC_User_XMPP extends \OCA\user_external\Base {
|
class XMPP extends Base {
|
||||||
private $host;
|
private $host;
|
||||||
private $xmppDb;
|
private $xmppDb;
|
||||||
private $xmppDbUser;
|
private $xmppDbUser;
|
||||||
@@ -96,7 +97,7 @@ class OC_User_XMPP extends \OCA\user_external\Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function checkPassword($uid, $password){
|
public function checkPassword($uid, $password){
|
||||||
$pdo = new PDO("mysql:host=$this->host;dbname=$this->xmppDb", $this->xmppDbUser, $this->xmppDbPassword);
|
$pdo = new \PDO("mysql:host=$this->host;dbname=$this->xmppDb", $this->xmppDbUser, $this->xmppDbPassword);
|
||||||
if(isset($uid)
|
if(isset($uid)
|
||||||
&& isset($password)) {
|
&& isset($password)) {
|
||||||
if(!filter_var($uid, FILTER_VALIDATE_EMAIL)
|
if(!filter_var($uid, FILTER_VALIDATE_EMAIL)
|
||||||
Reference in New Issue
Block a user