Merge "HTTP auth for Chromium HTTP stack (Java side)"
This commit is contained in:
committed by
Android (Google) Code Review
commit
4200dcaa7b
@@ -1031,6 +1031,57 @@ class BrowserFrame extends Handler {
|
||||
return mContext.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by JNI when the native HTTP stack gets an authentication request.
|
||||
*
|
||||
* We delegate the request to CallbackProxy, and route its response to
|
||||
* {@link #nativeAuthenticationProceed(int, String, String)} or
|
||||
* {@link #nativeAuthenticationCancel(int)}.
|
||||
*/
|
||||
private void didReceiveAuthenticationChallenge(
|
||||
final int handle, String host, String realm, final boolean useCachedCredentials) {
|
||||
|
||||
HttpAuthHandler handler = new HttpAuthHandler() {
|
||||
|
||||
private static final int AUTH_PROCEED = 1;
|
||||
private static final int AUTH_CANCEL = 2;
|
||||
|
||||
@Override
|
||||
public boolean useHttpAuthUsernamePassword() {
|
||||
return useCachedCredentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void proceed(String username, String password) {
|
||||
Message msg = obtainMessage(AUTH_PROCEED);
|
||||
msg.getData().putString("username", username);
|
||||
msg.getData().putString("password", password);
|
||||
sendMessage(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
sendMessage(obtainMessage(AUTH_CANCEL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case AUTH_PROCEED:
|
||||
String username = msg.getData().getString("username");
|
||||
String password = msg.getData().getString("password");
|
||||
nativeAuthenticationProceed(handle, username, password);
|
||||
break;
|
||||
|
||||
case AUTH_CANCEL:
|
||||
nativeAuthenticationCancel(handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
mCallbackProxy.onReceivedHttpAuthRequest(handler, host, realm);
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
// native functions
|
||||
//==========================================================================
|
||||
@@ -1147,4 +1198,7 @@ class BrowserFrame extends Handler {
|
||||
private native String nativeSaveWebArchive(String basename, boolean autoname);
|
||||
|
||||
private native void nativeOrientationChanged(int orientation);
|
||||
|
||||
private native void nativeAuthenticationProceed(int handle, String username, String password);
|
||||
private native void nativeAuthenticationCancel(int handle);
|
||||
}
|
||||
|
||||
@@ -16,151 +16,19 @@
|
||||
|
||||
package android.webkit;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ListIterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* HTTP authentication handler: local handler that takes care
|
||||
* of HTTP authentication requests. This class is passed as a
|
||||
* parameter to BrowserCallback.displayHttpAuthDialog and is
|
||||
* meant to receive the user's response.
|
||||
* HTTP authentication request that must be handled by the user interface.
|
||||
* WebView creates the object and hands it to the current {@link WebViewClient},
|
||||
* which must call either {@link #proceed(String, String)} or {@link #cancel()}.
|
||||
*/
|
||||
public class HttpAuthHandler extends Handler {
|
||||
/* It is important that the handler is in Network, because
|
||||
* we want to share it accross multiple loaders and windows
|
||||
* (like our subwindow and the main window).
|
||||
*/
|
||||
|
||||
private static final String LOGTAG = "network";
|
||||
|
||||
/**
|
||||
* Network.
|
||||
* Package-private constructor needed for API compatibility.
|
||||
*/
|
||||
private Network mNetwork;
|
||||
|
||||
/**
|
||||
* Loader queue.
|
||||
*/
|
||||
private LinkedList<LoadListener> mLoaderQueue;
|
||||
|
||||
|
||||
// Message id for handling the user response
|
||||
private static final int AUTH_PROCEED = 100;
|
||||
private static final int AUTH_CANCEL = 200;
|
||||
|
||||
// Use to synchronize when making synchronous calls to
|
||||
// onReceivedHttpAuthRequest(). We can't use a single Boolean object for
|
||||
// both the lock and the state, because Boolean is immutable.
|
||||
Object mRequestInFlightLock = new Object();
|
||||
boolean mRequestInFlight;
|
||||
String mUsername;
|
||||
String mPassword;
|
||||
|
||||
/**
|
||||
* Creates a new HTTP authentication handler with an empty
|
||||
* loader queue
|
||||
*
|
||||
* @param network The parent network object
|
||||
*/
|
||||
/* package */ HttpAuthHandler(Network network) {
|
||||
mNetwork = network;
|
||||
mLoaderQueue = new LinkedList<LoadListener>();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.poll();
|
||||
}
|
||||
assert(loader.isSynchronous() == false);
|
||||
|
||||
switch (msg.what) {
|
||||
case AUTH_PROCEED:
|
||||
String username = msg.getData().getString("username");
|
||||
String password = msg.getData().getString("password");
|
||||
|
||||
loader.handleAuthResponse(username, password);
|
||||
break;
|
||||
|
||||
case AUTH_CANCEL:
|
||||
loader.handleAuthResponse(null, null);
|
||||
break;
|
||||
}
|
||||
|
||||
processNextLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method used to unblock handleAuthRequest(), which in the case of a
|
||||
* synchronous request will wait for proxy.onReceivedHttpAuthRequest() to
|
||||
* call back to either proceed() or cancel().
|
||||
*
|
||||
* @param username The username to use for authentication
|
||||
* @param password The password to use for authentication
|
||||
* @return True if the request is synchronous and handleAuthRequest() has
|
||||
* been unblocked
|
||||
*/
|
||||
private boolean handleResponseForSynchronousRequest(String username, String password) {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader.isSynchronous()) {
|
||||
mUsername = username;
|
||||
mPassword = password;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void signalRequestComplete() {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
assert(mRequestInFlight);
|
||||
mRequestInFlight = false;
|
||||
mRequestInFlightLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed with the authorization with the given credentials
|
||||
*
|
||||
* May be called on the UI thread, rather than the WebCore thread.
|
||||
*
|
||||
* @param username The username to use for authentication
|
||||
* @param password The password to use for authentication
|
||||
*/
|
||||
public void proceed(String username, String password) {
|
||||
if (handleResponseForSynchronousRequest(username, password)) {
|
||||
signalRequestComplete();
|
||||
return;
|
||||
}
|
||||
Message msg = obtainMessage(AUTH_PROCEED);
|
||||
msg.getData().putString("username", username);
|
||||
msg.getData().putString("password", password);
|
||||
sendMessage(msg);
|
||||
signalRequestComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the authorization request
|
||||
*
|
||||
* May be called on the UI thread, rather than the WebCore thread.
|
||||
*
|
||||
*/
|
||||
public void cancel() {
|
||||
if (handleResponseForSynchronousRequest(null, null)) {
|
||||
signalRequestComplete();
|
||||
return;
|
||||
}
|
||||
sendMessage(obtainMessage(AUTH_CANCEL));
|
||||
signalRequestComplete();
|
||||
HttpAuthHandler() {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,113 +36,18 @@ public class HttpAuthHandler extends Handler {
|
||||
* (ie, if we did not fail trying to use them last time)
|
||||
*/
|
||||
public boolean useHttpAuthUsernamePassword() {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader != null) {
|
||||
return !loader.authCredentialsInvalid();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the loader, if the loader is the only element
|
||||
* in the queue, starts processing the loader
|
||||
*
|
||||
* @param loader The loader that resulted in this http
|
||||
* authentication request
|
||||
* Cancel the authorization request.
|
||||
*/
|
||||
/* package */ void handleAuthRequest(LoadListener loader) {
|
||||
// The call to proxy.onReceivedHttpAuthRequest() may be asynchronous. If
|
||||
// the request is synchronous, we must block here until we have a
|
||||
// response.
|
||||
if (loader.isSynchronous()) {
|
||||
// If there's a request in flight, wait for it to complete. The
|
||||
// response will queue a message on this thread.
|
||||
waitForRequestToComplete();
|
||||
// Make a request to the proxy for this request, jumping the queue.
|
||||
// We use the queue so that the loader is present in
|
||||
// useHttpAuthUsernamePassword().
|
||||
synchronized (mLoaderQueue) {
|
||||
mLoaderQueue.addFirst(loader);
|
||||
}
|
||||
processNextLoader();
|
||||
// Wait for this request to complete.
|
||||
waitForRequestToComplete();
|
||||
// Pop the loader from the queue.
|
||||
synchronized (mLoaderQueue) {
|
||||
assert(mLoaderQueue.peek() == loader);
|
||||
mLoaderQueue.poll();
|
||||
}
|
||||
// Call back.
|
||||
loader.handleAuthResponse(mUsername, mPassword);
|
||||
// The message queued by the response from the last asynchronous
|
||||
// request, if present, will start the next request.
|
||||
return;
|
||||
}
|
||||
|
||||
boolean processNext = false;
|
||||
|
||||
synchronized (mLoaderQueue) {
|
||||
mLoaderQueue.offer(loader);
|
||||
processNext =
|
||||
(mLoaderQueue.size() == 1);
|
||||
}
|
||||
|
||||
if (processNext) {
|
||||
processNextLoader();
|
||||
}
|
||||
public void cancel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the request in flight, if any, to complete
|
||||
* Proceed with the authorization with the given credentials.
|
||||
*/
|
||||
private void waitForRequestToComplete() {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
while (mRequestInFlight) {
|
||||
try {
|
||||
mRequestInFlightLock.wait();
|
||||
} catch(InterruptedException e) {
|
||||
Log.e(LOGTAG, "Interrupted while waiting for request to complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next loader in the queue (helper method)
|
||||
*/
|
||||
private void processNextLoader() {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader != null) {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
assert(mRequestInFlight == false);
|
||||
mRequestInFlight = true;
|
||||
}
|
||||
|
||||
CallbackProxy proxy = loader.getFrame().getCallbackProxy();
|
||||
|
||||
String hostname = loader.proxyAuthenticate() ?
|
||||
mNetwork.getProxyHostname() : loader.host();
|
||||
|
||||
String realm = loader.realm();
|
||||
|
||||
proxy.onReceivedHttpAuthRequest(this, hostname, realm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the WebView of a new set of credentials.
|
||||
* @hide Pending API council review
|
||||
*/
|
||||
public static void onReceivedCredentials(LoadListener loader,
|
||||
String host, String realm, String username, String password) {
|
||||
CallbackProxy proxy = loader.getFrame().getCallbackProxy();
|
||||
proxy.onReceivedHttpAuthCredentials(host, realm, username, password);
|
||||
public void proceed(String username, String password) {
|
||||
}
|
||||
}
|
||||
|
||||
280
core/java/android/webkit/HttpAuthHandlerImpl.java
Normal file
280
core/java/android/webkit/HttpAuthHandlerImpl.java
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.webkit;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ListIterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* HttpAuthHandler implementation is used only by the Android Java HTTP stack.
|
||||
* <p>
|
||||
* This class is not needed when we're using the Chromium HTTP stack.
|
||||
*/
|
||||
class HttpAuthHandlerImpl extends HttpAuthHandler {
|
||||
/*
|
||||
* It is important that the handler is in Network, because we want to share
|
||||
* it accross multiple loaders and windows (like our subwindow and the main
|
||||
* window).
|
||||
*/
|
||||
|
||||
private static final String LOGTAG = "network";
|
||||
|
||||
/**
|
||||
* Network.
|
||||
*/
|
||||
private Network mNetwork;
|
||||
|
||||
/**
|
||||
* Loader queue.
|
||||
*/
|
||||
private LinkedList<LoadListener> mLoaderQueue;
|
||||
|
||||
|
||||
// Message id for handling the user response
|
||||
private static final int AUTH_PROCEED = 100;
|
||||
private static final int AUTH_CANCEL = 200;
|
||||
|
||||
// Use to synchronize when making synchronous calls to
|
||||
// onReceivedHttpAuthRequest(). We can't use a single Boolean object for
|
||||
// both the lock and the state, because Boolean is immutable.
|
||||
Object mRequestInFlightLock = new Object();
|
||||
boolean mRequestInFlight;
|
||||
String mUsername;
|
||||
String mPassword;
|
||||
|
||||
/**
|
||||
* Creates a new HTTP authentication handler with an empty
|
||||
* loader queue
|
||||
*
|
||||
* @param network The parent network object
|
||||
*/
|
||||
/* package */ HttpAuthHandlerImpl(Network network) {
|
||||
mNetwork = network;
|
||||
mLoaderQueue = new LinkedList<LoadListener>();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.poll();
|
||||
}
|
||||
assert(loader.isSynchronous() == false);
|
||||
|
||||
switch (msg.what) {
|
||||
case AUTH_PROCEED:
|
||||
String username = msg.getData().getString("username");
|
||||
String password = msg.getData().getString("password");
|
||||
|
||||
loader.handleAuthResponse(username, password);
|
||||
break;
|
||||
|
||||
case AUTH_CANCEL:
|
||||
loader.handleAuthResponse(null, null);
|
||||
break;
|
||||
}
|
||||
|
||||
processNextLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method used to unblock handleAuthRequest(), which in the case of a
|
||||
* synchronous request will wait for proxy.onReceivedHttpAuthRequest() to
|
||||
* call back to either proceed() or cancel().
|
||||
*
|
||||
* @param username The username to use for authentication
|
||||
* @param password The password to use for authentication
|
||||
* @return True if the request is synchronous and handleAuthRequest() has
|
||||
* been unblocked
|
||||
*/
|
||||
private boolean handleResponseForSynchronousRequest(String username, String password) {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader.isSynchronous()) {
|
||||
mUsername = username;
|
||||
mPassword = password;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void signalRequestComplete() {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
assert(mRequestInFlight);
|
||||
mRequestInFlight = false;
|
||||
mRequestInFlightLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed with the authorization with the given credentials
|
||||
*
|
||||
* May be called on the UI thread, rather than the WebCore thread.
|
||||
*
|
||||
* @param username The username to use for authentication
|
||||
* @param password The password to use for authentication
|
||||
*/
|
||||
public void proceed(String username, String password) {
|
||||
if (handleResponseForSynchronousRequest(username, password)) {
|
||||
signalRequestComplete();
|
||||
return;
|
||||
}
|
||||
Message msg = obtainMessage(AUTH_PROCEED);
|
||||
msg.getData().putString("username", username);
|
||||
msg.getData().putString("password", password);
|
||||
sendMessage(msg);
|
||||
signalRequestComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the authorization request
|
||||
*
|
||||
* May be called on the UI thread, rather than the WebCore thread.
|
||||
*
|
||||
*/
|
||||
public void cancel() {
|
||||
if (handleResponseForSynchronousRequest(null, null)) {
|
||||
signalRequestComplete();
|
||||
return;
|
||||
}
|
||||
sendMessage(obtainMessage(AUTH_CANCEL));
|
||||
signalRequestComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if we can use user credentials on record
|
||||
* (ie, if we did not fail trying to use them last time)
|
||||
*/
|
||||
public boolean useHttpAuthUsernamePassword() {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader != null) {
|
||||
return !loader.authCredentialsInvalid();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the loader, if the loader is the only element
|
||||
* in the queue, starts processing the loader
|
||||
*
|
||||
* @param loader The loader that resulted in this http
|
||||
* authentication request
|
||||
*/
|
||||
/* package */ void handleAuthRequest(LoadListener loader) {
|
||||
// The call to proxy.onReceivedHttpAuthRequest() may be asynchronous. If
|
||||
// the request is synchronous, we must block here until we have a
|
||||
// response.
|
||||
if (loader.isSynchronous()) {
|
||||
// If there's a request in flight, wait for it to complete. The
|
||||
// response will queue a message on this thread.
|
||||
waitForRequestToComplete();
|
||||
// Make a request to the proxy for this request, jumping the queue.
|
||||
// We use the queue so that the loader is present in
|
||||
// useHttpAuthUsernamePassword().
|
||||
synchronized (mLoaderQueue) {
|
||||
mLoaderQueue.addFirst(loader);
|
||||
}
|
||||
processNextLoader();
|
||||
// Wait for this request to complete.
|
||||
waitForRequestToComplete();
|
||||
// Pop the loader from the queue.
|
||||
synchronized (mLoaderQueue) {
|
||||
assert(mLoaderQueue.peek() == loader);
|
||||
mLoaderQueue.poll();
|
||||
}
|
||||
// Call back.
|
||||
loader.handleAuthResponse(mUsername, mPassword);
|
||||
// The message queued by the response from the last asynchronous
|
||||
// request, if present, will start the next request.
|
||||
return;
|
||||
}
|
||||
|
||||
boolean processNext = false;
|
||||
|
||||
synchronized (mLoaderQueue) {
|
||||
mLoaderQueue.offer(loader);
|
||||
processNext =
|
||||
(mLoaderQueue.size() == 1);
|
||||
}
|
||||
|
||||
if (processNext) {
|
||||
processNextLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the request in flight, if any, to complete
|
||||
*/
|
||||
private void waitForRequestToComplete() {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
while (mRequestInFlight) {
|
||||
try {
|
||||
mRequestInFlightLock.wait();
|
||||
} catch(InterruptedException e) {
|
||||
Log.e(LOGTAG, "Interrupted while waiting for request to complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next loader in the queue (helper method)
|
||||
*/
|
||||
private void processNextLoader() {
|
||||
LoadListener loader = null;
|
||||
synchronized (mLoaderQueue) {
|
||||
loader = mLoaderQueue.peek();
|
||||
}
|
||||
if (loader != null) {
|
||||
synchronized (mRequestInFlightLock) {
|
||||
assert(mRequestInFlight == false);
|
||||
mRequestInFlight = true;
|
||||
}
|
||||
|
||||
CallbackProxy proxy = loader.getFrame().getCallbackProxy();
|
||||
|
||||
String hostname = loader.proxyAuthenticate() ?
|
||||
mNetwork.getProxyHostname() : loader.host();
|
||||
|
||||
String realm = loader.realm();
|
||||
|
||||
proxy.onReceivedHttpAuthRequest(this, hostname, realm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the WebView of a new set of credentials.
|
||||
* @hide Pending API council review
|
||||
*/
|
||||
public static void onReceivedCredentials(LoadListener loader,
|
||||
String host, String realm, String username, String password) {
|
||||
CallbackProxy proxy = loader.getFrame().getCallbackProxy();
|
||||
proxy.onReceivedHttpAuthCredentials(host, realm, username, password);
|
||||
}
|
||||
}
|
||||
@@ -683,7 +683,7 @@ class LoadListener extends Handler implements EventHandler {
|
||||
String host = mAuthHeader.isProxy() ?
|
||||
Network.getInstance(mContext).getProxyHostname() :
|
||||
mUri.mHost;
|
||||
HttpAuthHandler.onReceivedCredentials(this, host,
|
||||
HttpAuthHandlerImpl.onReceivedCredentials(this, host,
|
||||
mAuthHeader.getRealm(), mUsername, mPassword);
|
||||
makeAuthResponse(mUsername, mPassword);
|
||||
} else {
|
||||
|
||||
@@ -79,7 +79,7 @@ class Network {
|
||||
* HTTP authentication handler: takes care of synchronization of HTTP
|
||||
* authentication requests.
|
||||
*/
|
||||
private HttpAuthHandler mHttpAuthHandler;
|
||||
private HttpAuthHandlerImpl mHttpAuthHandler;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@@ -158,7 +158,7 @@ class Network {
|
||||
}
|
||||
mContext = context;
|
||||
mSslErrorHandler = new SslErrorHandler();
|
||||
mHttpAuthHandler = new HttpAuthHandler(this);
|
||||
mHttpAuthHandler = new HttpAuthHandlerImpl(this);
|
||||
|
||||
mRequestQueue = new RequestQueue(context);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user