mirror of
https://github.com/owncloudarchive/contacts.git
synced 2024-12-10 21:24:33 +01:00
Implement HTTP Middleware
Intercept exceptions thrown and return suitable HTTP responses.
This commit is contained in:
parent
f67d18b894
commit
cab617f0e0
@ -149,10 +149,7 @@ OC.notify = function(params) {
|
|||||||
OC.Contacts = OC.Contacts || {
|
OC.Contacts = OC.Contacts || {
|
||||||
init:function() {
|
init:function() {
|
||||||
if(oc_debug === true) {
|
if(oc_debug === true) {
|
||||||
$.error = console.error;
|
$.error = console.error;
|
||||||
$(document).ajaxError(function(e, xhr, settings, exception) {
|
|
||||||
console.error('Error in: ', settings.url, ' : ', xhr.responseText, exception);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -2221,10 +2221,9 @@ OC.Contacts = OC.Contacts || {};
|
|||||||
defer.reject(response);
|
defer.reject(response);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(function(jqxhr, textStatus, error) {
|
.fail(function(response) {
|
||||||
var err = textStatus + ', ' + error;
|
console.warn('Request Failed:', response.message);
|
||||||
console.warn( "Request Failed: " + err);
|
this.reject({error: true, message: response.message});
|
||||||
this.reject({error: true, message: err});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,6 +24,15 @@ OC.Contacts = OC.Contacts || {};
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ajaxError(function(e, xhr, settings, exception) {
|
||||||
|
console.error('Error in: ', settings.url, ' : ', xhr.responseText, exception);
|
||||||
|
var response = $.parseJSON(xhr.responseText);
|
||||||
|
console.log('response', response);
|
||||||
|
$(document).trigger('status.contact.error', {
|
||||||
|
message: response ? new JSONResponse(response, xhr).message : xhr.responseText
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object for saving contact data to backends
|
* An object for saving contact data to backends
|
||||||
*
|
*
|
||||||
|
@ -25,8 +25,16 @@ namespace OCA\Contacts;
|
|||||||
/**
|
/**
|
||||||
* This class manages our addressbooks.
|
* This class manages our addressbooks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Addressbook extends AbstractPIMCollection {
|
class Addressbook extends AbstractPIMCollection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief language object
|
||||||
|
*
|
||||||
|
* @var OC_L10N
|
||||||
|
*/
|
||||||
|
public static $l10n;
|
||||||
|
|
||||||
protected $_count;
|
protected $_count;
|
||||||
/**
|
/**
|
||||||
* @var Backend\AbstractBackend
|
* @var Backend\AbstractBackend
|
||||||
@ -52,12 +60,13 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
* @param array $addressBookInfo
|
* @param array $addressBookInfo
|
||||||
*/
|
*/
|
||||||
public function __construct(Backend\AbstractBackend $backend, array $addressBookInfo) {
|
public function __construct(Backend\AbstractBackend $backend, array $addressBookInfo) {
|
||||||
|
self::$l10n = \OC_L10N::get('contacts');
|
||||||
$this->backend = $backend;
|
$this->backend = $backend;
|
||||||
$this->addressBookInfo = $addressBookInfo;
|
$this->addressBookInfo = $addressBookInfo;
|
||||||
if(is_null($this->getId())) {
|
if(is_null($this->getId())) {
|
||||||
$id = $this->backend->createAddressBook($addressBookInfo);
|
$id = $this->backend->createAddressBook($addressBookInfo);
|
||||||
if($id === false) {
|
if($id === false) {
|
||||||
throw new \Exception('Error creating address book.');
|
throw new \Exception('Error creating address book.', 500);
|
||||||
}
|
}
|
||||||
$this->addressBookInfo = $this->backend->getAddressBook($id);
|
$this->addressBookInfo = $this->backend->getAddressBook($id);
|
||||||
//print(__METHOD__. ' '. __LINE__ . ' addressBookInfo: ' . print_r($this->backend->getAddressBook($id), true));
|
//print(__METHOD__. ' '. __LINE__ . ' addressBookInfo: ' . print_r($this->backend->getAddressBook($id), true));
|
||||||
@ -127,7 +136,7 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
function getChild($id) {
|
function getChild($id) {
|
||||||
//\OCP\Util::writeLog('contacts', __METHOD__.' id: '.$id, \OCP\Util::DEBUG);
|
//\OCP\Util::writeLog('contacts', __METHOD__.' id: '.$id, \OCP\Util::DEBUG);
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to see this contacts'), 403);
|
||||||
}
|
}
|
||||||
if(!isset($this->objects[$id])) {
|
if(!isset($this->objects[$id])) {
|
||||||
$contact = $this->backend->getContact($this->getId(), $id);
|
$contact = $this->backend->getContact($this->getId(), $id);
|
||||||
@ -159,7 +168,7 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
*/
|
*/
|
||||||
function getChildren($limit = null, $offset = null, $omitdata = false) {
|
function getChildren($limit = null, $offset = null, $omitdata = false) {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to see these contacts'), 403);
|
||||||
}
|
}
|
||||||
//\OCP\Util::writeLog('contacts', __METHOD__.' backend: ' . print_r($this->backend, true), \OCP\Util::DEBUG);
|
//\OCP\Util::writeLog('contacts', __METHOD__.' backend: ' . print_r($this->backend, true), \OCP\Util::DEBUG);
|
||||||
$contacts = array();
|
$contacts = array();
|
||||||
@ -185,10 +194,10 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
*/
|
*/
|
||||||
public function addChild($data = null) {
|
public function addChild($data = null) {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_CREATE)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_CREATE)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions add contacts to the address book'), 403);
|
||||||
}
|
}
|
||||||
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_CREATE)) {
|
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_CREATE)) {
|
||||||
throw new \Exception('Not implemented');
|
throw new \Exception(self::$l10n('The backend for this address book does not support adding contacts'), 501);
|
||||||
}
|
}
|
||||||
$contact = new Contact($this, $this->backend, $data);
|
$contact = new Contact($this, $this->backend, $data);
|
||||||
if($contact->save() === false) {
|
if($contact->save() === false) {
|
||||||
@ -210,10 +219,10 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
*/
|
*/
|
||||||
public function deleteChild($id) {
|
public function deleteChild($id) {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_DELETE)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_DELETE)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to delete this contact'), 403);
|
||||||
}
|
}
|
||||||
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_DELETE)) {
|
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_DELETE)) {
|
||||||
throw new \Exception('Not implemented');
|
throw new \Exception(self::$l10n('The backend for this address book does not support deleting contacts'), 501);
|
||||||
}
|
}
|
||||||
if($this->backend->deleteContact($this->getId(), $id)) {
|
if($this->backend->deleteContact($this->getId(), $id)) {
|
||||||
if(isset($this->objects[$id])) {
|
if(isset($this->objects[$id])) {
|
||||||
@ -273,7 +282,7 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
*/
|
*/
|
||||||
public function save() {
|
public function save() {
|
||||||
if(!$this->hasPermission(OCP\PERMISSION_UPDATE)) {
|
if(!$this->hasPermission(OCP\PERMISSION_UPDATE)) {
|
||||||
throw new Exception('You don\'t have permissions to update the address book.');
|
throw new Exception(self::$l10n('You don\'t have permissions to update the address book.'), 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +293,7 @@ class Addressbook extends AbstractPIMCollection {
|
|||||||
*/
|
*/
|
||||||
public function delete() {
|
public function delete() {
|
||||||
if(!$this->hasPermission(OCP\PERMISSION_DELETE)) {
|
if(!$this->hasPermission(OCP\PERMISSION_DELETE)) {
|
||||||
throw new Exception('You don\'t have permissions to delete the address book.');
|
throw new Exception(self::$l10n('You don\'t have permissions to delete the address book.'), 403);
|
||||||
}
|
}
|
||||||
return $this->backend->deleteAddressBook($this->getId());
|
return $this->backend->deleteAddressBook($this->getId());
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,13 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
*/
|
*/
|
||||||
public $name = 'VCARD';
|
public $name = 'VCARD';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief language object
|
||||||
|
*
|
||||||
|
* @var OC_L10N
|
||||||
|
*/
|
||||||
|
public static $l10n;
|
||||||
|
|
||||||
protected $props = array();
|
protected $props = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,6 +59,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
* @param mixed $data
|
* @param mixed $data
|
||||||
*/
|
*/
|
||||||
public function __construct($parent, $backend, $data = null) {
|
public function __construct($parent, $backend, $data = null) {
|
||||||
|
self::$l10n = $parent::$l10n;
|
||||||
//\OCP\Util::writeLog('contacts', __METHOD__ . ' parent: ' . print_r($parent, true) . ', backend: ' . print_r($backend, true) . ', data: ' . print_r($data, true), \OCP\Util::DEBUG);
|
//\OCP\Util::writeLog('contacts', __METHOD__ . ' parent: ' . print_r($parent, true) . ', backend: ' . print_r($backend, true) . ', data: ' . print_r($data, true), \OCP\Util::DEBUG);
|
||||||
//\OCP\Util::writeLog('contacts', __METHOD__, \OCP\Util::DEBUG);
|
//\OCP\Util::writeLog('contacts', __METHOD__, \OCP\Util::DEBUG);
|
||||||
$this->props['parent'] = $parent;
|
$this->props['parent'] = $parent;
|
||||||
@ -101,7 +109,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
*/
|
*/
|
||||||
public function getMetaData() {
|
public function getMetaData() {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to see this contacts'), 403);
|
||||||
}
|
}
|
||||||
if(!isset($this->props['displayname'])) {
|
if(!isset($this->props['displayname'])) {
|
||||||
if(!$this->retrieve()) {
|
if(!$this->retrieve()) {
|
||||||
@ -148,7 +156,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
*/
|
*/
|
||||||
function getDisplayName() {
|
function getDisplayName() {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_READ)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to see this contacts'), 403);
|
||||||
}
|
}
|
||||||
return isset($this->props['displayname']) ? $this->props['displayname'] : null;
|
return isset($this->props['displayname']) ? $this->props['displayname'] : null;
|
||||||
}
|
}
|
||||||
@ -231,7 +239,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
*/
|
*/
|
||||||
public function delete() {
|
public function delete() {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_DELETE)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_DELETE)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to delete this contact'), 403);
|
||||||
}
|
}
|
||||||
return $this->props['backend']->deleteContact(
|
return $this->props['backend']->deleteContact(
|
||||||
$this->getParent()->getId(),
|
$this->getParent()->getId(),
|
||||||
@ -246,7 +254,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
*/
|
*/
|
||||||
public function save($force = false) {
|
public function save($force = false) {
|
||||||
if(!$this->hasPermission(\OCP\PERMISSION_UPDATE)) {
|
if(!$this->hasPermission(\OCP\PERMISSION_UPDATE)) {
|
||||||
throw new \Exception('Access denied');
|
throw new \Exception(self::$l10n('You do not have permissions to update this contact'), 403);
|
||||||
}
|
}
|
||||||
if($this->isSaved() && !$force) {
|
if($this->isSaved() && !$force) {
|
||||||
\OCP\Util::writeLog('contacts', __METHOD__.' Already saved: ' . print_r($this->props, true), \OCP\Util::DEBUG);
|
\OCP\Util::writeLog('contacts', __METHOD__.' Already saved: ' . print_r($this->props, true), \OCP\Util::DEBUG);
|
||||||
@ -257,7 +265,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
}
|
}
|
||||||
if($this->getId()) {
|
if($this->getId()) {
|
||||||
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_UPDATE)) {
|
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_UPDATE)) {
|
||||||
throw new \Exception('Not implemented');
|
throw new \Exception(self::$l10n('The backend for this contact does not support updating it'), 501);
|
||||||
}
|
}
|
||||||
if($this->getBackend()
|
if($this->getBackend()
|
||||||
->updateContact(
|
->updateContact(
|
||||||
@ -275,7 +283,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
} else {
|
} else {
|
||||||
//print(__METHOD__.' ' . print_r($this->getParent(), true));
|
//print(__METHOD__.' ' . print_r($this->getParent(), true));
|
||||||
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_CREATE)) {
|
if(!$this->getBackend()->hasContactMethodFor(\OCP\PERMISSION_CREATE)) {
|
||||||
throw new \Exception('Not implemented');
|
throw new \Exception(self::$l10n('This not support adding contacts'), 501);
|
||||||
}
|
}
|
||||||
$this->props['id'] = $this->getBackend()->createContact(
|
$this->props['id'] = $this->getBackend()->createContact(
|
||||||
$this->getParent()->getId(), $this
|
$this->getParent()->getId(), $this
|
||||||
@ -384,7 +392,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
}
|
}
|
||||||
$idx += 1;
|
$idx += 1;
|
||||||
}
|
}
|
||||||
throw new Exception('Property not found', 404);
|
throw new Exception(self::$l10n('Property not found'), 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -401,7 +409,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
return $property;
|
return $property;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new \Exception('Property not found', 404);
|
throw new \Exception(self::$l10n('Property not found'), 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -449,7 +457,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
break;
|
break;
|
||||||
case 'IMPP':
|
case 'IMPP':
|
||||||
if(is_null($parameters) || !isset($parameters['X-SERVICE-TYPE'])) {
|
if(is_null($parameters) || !isset($parameters['X-SERVICE-TYPE'])) {
|
||||||
throw new \InvalidArgumentException(__METHOD__.' Missing IM parameter for: '.$name. ' ' . $value);
|
throw new \InvalidArgumentException(self::$l10n(' Missing IM parameter for: ') . $name. ' ' . $value, 412);
|
||||||
}
|
}
|
||||||
$serviceType = $parameters['X-SERVICE-TYPE'];
|
$serviceType = $parameters['X-SERVICE-TYPE'];
|
||||||
if(is_array($serviceType)) {
|
if(is_array($serviceType)) {
|
||||||
@ -457,7 +465,7 @@ class Contact extends VObject\VCard implements IPIMObject {
|
|||||||
}
|
}
|
||||||
$impp = Utils\Properties::getIMOptions($serviceType);
|
$impp = Utils\Properties::getIMOptions($serviceType);
|
||||||
if(is_null($impp)) {
|
if(is_null($impp)) {
|
||||||
throw new \UnexpectedValueException(__METHOD__.' Unknown IM: ' . $serviceType);
|
throw new \UnexpectedValueException(self::$l10n('Unknown IM: ') . $serviceType, 415);
|
||||||
}
|
}
|
||||||
$value = $impp['protocol'] . ':' . $value;
|
$value = $impp['protocol'] . ':' . $value;
|
||||||
$property->setValue($value);
|
$property->setValue($value);
|
||||||
|
@ -8,7 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
namespace OCA\Contacts;
|
namespace OCA\Contacts;
|
||||||
|
|
||||||
use OCA\AppFramework\DependencyInjection\DIContainer as BaseContainer;
|
use OCA\AppFramework\DependencyInjection\DIContainer as BaseContainer;
|
||||||
|
use OCA\AppFramework\Middleware\MiddlewareDispatcher;
|
||||||
|
use OCA\AppFramework\Middleware\Security\SecurityMiddleware;
|
||||||
|
use OCA\Contacts\Middleware\Http as HttpMiddleware;
|
||||||
use OCA\Contacts\Controller\AddressBookController;
|
use OCA\Contacts\Controller\AddressBookController;
|
||||||
use OCA\Contacts\Controller\GroupController;
|
use OCA\Contacts\Controller\GroupController;
|
||||||
use OCA\Contacts\Controller\ContactController;
|
use OCA\Contacts\Controller\ContactController;
|
||||||
@ -25,6 +29,18 @@ class DIContainer extends BaseContainer {
|
|||||||
// tell parent container about the app name
|
// tell parent container about the app name
|
||||||
parent::__construct('contacts');
|
parent::__construct('contacts');
|
||||||
|
|
||||||
|
$this['HttpMiddleware'] = $this->share(function($c){
|
||||||
|
return new HttpMiddleware($c['API']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this['MiddlewareDispatcher'] = $this->share(function($c){
|
||||||
|
$dispatcher = new MiddlewareDispatcher();
|
||||||
|
$dispatcher->registerMiddleware($c['HttpMiddleware']);
|
||||||
|
$dispatcher->registerMiddleware($c['SecurityMiddleware']);
|
||||||
|
|
||||||
|
return $dispatcher;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CONTROLLERS
|
* CONTROLLERS
|
||||||
*/
|
*/
|
||||||
|
69
lib/middleware/http.php
Normal file
69
lib/middleware/http.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ownCloud - HTTP Middleware
|
||||||
|
*
|
||||||
|
* @author Thomas Tanghus
|
||||||
|
* @copyright 2013 Thomas Tanghus (thomas@tanghus.net)
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 3 of the License, or any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public
|
||||||
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Contacts\Middleware;
|
||||||
|
|
||||||
|
use OCA\AppFramework\Controller\Controller;
|
||||||
|
use OCA\AppFramework\Middleware\Middleware;
|
||||||
|
use OCA\AppFramework\Core\API;
|
||||||
|
use OCA\Contacts\JSONResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to intercept exceptions thrown in controllers and backends
|
||||||
|
* and transform them into valid HTTP responses.
|
||||||
|
*/
|
||||||
|
class HTTP extends Middleware {
|
||||||
|
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param API $api an instance of the api
|
||||||
|
*/
|
||||||
|
public function __construct(API $api){
|
||||||
|
$this->api = $api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an Exception is being caught, return a JSON error response with
|
||||||
|
* a suitable status code
|
||||||
|
* @param Controller $controller the controller that is being called
|
||||||
|
* @param string $methodName the name of the method that will be called on
|
||||||
|
* the controller
|
||||||
|
* @param \Exception $exception the thrown exception
|
||||||
|
* @return Response a Response object
|
||||||
|
*/
|
||||||
|
public function afterException($controller, $methodName, \Exception $exception){
|
||||||
|
// If there's no proper status code associated, set it to 500.
|
||||||
|
$response = new JSONResponse();
|
||||||
|
if($exception->getCode() < 100) {
|
||||||
|
$response->setStatus(500);
|
||||||
|
} else {
|
||||||
|
$response->setStatus($exception->getCode());
|
||||||
|
}
|
||||||
|
$response->setErrorMessage($exception->getMessage());
|
||||||
|
$this->api->log(get_class($controller) . '->' . $methodName . ': ' . $exception->getMessage());
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user