mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1108009 - Make synchronous interface nsIURIClassifier.ClassifyLocal. r=gcp
This commit is contained in:
parent
a3c98db7d8
commit
56633ce4b4
@ -30,7 +30,7 @@ interface nsIURIClassifierCallback : nsISupports
|
||||
* The URI classifier service checks a URI against lists of phishing
|
||||
* and malware sites.
|
||||
*/
|
||||
[scriptable, uuid(de4f03cd-1a28-4f51-906b-c54b47a533c5)]
|
||||
[scriptable, uuid(03d26681-0ef5-4718-9777-33c9e1ee3e32)]
|
||||
interface nsIURIClassifier : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -54,4 +54,12 @@ interface nsIURIClassifier : nsISupports
|
||||
boolean classify(in nsIPrincipal aPrincipal,
|
||||
in boolean aTrackingProtectionEnabled,
|
||||
in nsIURIClassifierCallback aCallback);
|
||||
|
||||
/**
|
||||
* Synchronously classify a Principal locally using its URI. This does not
|
||||
* make network requests. The result is an error code with which the channel
|
||||
* should be cancelled, or NS_OK if no result was found.
|
||||
*/
|
||||
nsresult classifyLocal(in nsIPrincipal aPrincipal,
|
||||
in boolean aTrackingProtectionEnabled);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "nsIInputStream.h"
|
||||
#include "nsISeekableStream.h"
|
||||
#include "nsIFile.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "mozilla/Telemetry.h"
|
||||
#include "prlog.h"
|
||||
|
||||
@ -54,7 +55,6 @@ Classifier::SplitTables(const nsACString& str, nsTArray<nsCString>& tables)
|
||||
}
|
||||
|
||||
Classifier::Classifier()
|
||||
: mFreshTime(45 * 60)
|
||||
{
|
||||
}
|
||||
|
||||
@ -144,6 +144,9 @@ Classifier::Open(nsIFile& aCacheDirectory)
|
||||
rv = CreateStoreDirectory();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Classifier keeps its own cryptoHash for doing operations on the background
|
||||
// thread. Callers can optionally pass in an nsICryptoHash for working on the
|
||||
// main thread.
|
||||
mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
@ -216,8 +219,15 @@ Classifier::TableRequest(nsACString& aResult)
|
||||
nsresult
|
||||
Classifier::Check(const nsACString& aSpec,
|
||||
const nsACString& aTables,
|
||||
uint32_t aFreshnessGuarantee,
|
||||
nsICryptoHash* aCryptoHash,
|
||||
LookupResultArray& aResults)
|
||||
{
|
||||
nsCOMPtr<nsICryptoHash> cryptoHash = aCryptoHash;
|
||||
if (!aCryptoHash) {
|
||||
MOZ_ASSERT(!NS_IsMainThread(), "mCryptoHash must be used on worker thread");
|
||||
cryptoHash = mCryptoHash;
|
||||
}
|
||||
Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_CL_CHECK_TIME> timer;
|
||||
|
||||
// Get the set of fragments based on the url. This is necessary because we
|
||||
@ -244,11 +254,11 @@ Classifier::Check(const nsACString& aSpec,
|
||||
// Now check each lookup fragment against the entries in the DB.
|
||||
for (uint32_t i = 0; i < fragments.Length(); i++) {
|
||||
Completion lookupHash;
|
||||
lookupHash.FromPlaintext(fragments[i], mCryptoHash);
|
||||
lookupHash.FromPlaintext(fragments[i], cryptoHash);
|
||||
|
||||
// Get list of host keys to look up
|
||||
Completion hostKey;
|
||||
rv = LookupCache::GetKey(fragments[i], &hostKey, mCryptoHash);
|
||||
rv = LookupCache::GetKey(fragments[i], &hostKey, cryptoHash);
|
||||
if (NS_FAILED(rv)) {
|
||||
// Local host on the network.
|
||||
continue;
|
||||
@ -288,7 +298,7 @@ Classifier::Check(const nsACString& aSpec,
|
||||
|
||||
result->hash.complete = lookupHash;
|
||||
result->mComplete = complete;
|
||||
result->mFresh = (age < mFreshTime);
|
||||
result->mFresh = (age < aFreshnessGuarantee);
|
||||
result->mTableName.Assign(cache->TableName());
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ public:
|
||||
*/
|
||||
nsresult Check(const nsACString& aSpec,
|
||||
const nsACString& tables,
|
||||
uint32_t aFreshnessGuarantee,
|
||||
nsICryptoHash* aCryptoHash,
|
||||
LookupResultArray& aResults);
|
||||
|
||||
/**
|
||||
@ -61,7 +63,6 @@ public:
|
||||
nsresult MarkSpoiled(nsTArray<nsCString>& aTables);
|
||||
nsresult CacheCompletions(const CacheResultArray& aResults);
|
||||
uint32_t GetHashKey(void) { return mHashKey; }
|
||||
void SetFreshTime(uint32_t aTime) { mFreshTime = aTime; }
|
||||
/*
|
||||
* Get a bunch of extra prefixes to query for completion
|
||||
* and mask the real entry being requested
|
||||
@ -102,7 +103,6 @@ private:
|
||||
uint32_t mHashKey;
|
||||
// Stores the last time a given table was updated (seconds).
|
||||
nsDataHashtable<nsCStringHashKey, int64_t> mTableFreshness;
|
||||
uint32_t mFreshTime;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -185,9 +185,11 @@ interface nsIUrlClassifierDBService : nsISupports
|
||||
* Interface for the actual worker thread. Implementations of this need not
|
||||
* be thread aware and just work on the database.
|
||||
*/
|
||||
[scriptable, uuid(abcd7978-c304-4a7d-a44c-33c2ed5441e7)]
|
||||
[scriptable, uuid(b7b505d0-bfa2-44db-abf8-6e2bfc25bbab)]
|
||||
interface nsIUrlClassifierDBServiceWorker : nsIUrlClassifierDBService
|
||||
{
|
||||
// Open the DB connection
|
||||
void openDb();
|
||||
// Provide a way to forcibly close the db connection.
|
||||
void closeDb();
|
||||
|
||||
|
@ -121,6 +121,13 @@ public:
|
||||
// update operations to prevent lookups from blocking for too long.
|
||||
nsresult HandlePendingLookups();
|
||||
|
||||
// Perform a blocking classifier lookup for a given url. Can be called on
|
||||
// either the main thread or the worker thread.
|
||||
nsresult DoLocalLookup(const nsACString& spec,
|
||||
const nsACString& tables,
|
||||
nsICryptoHash* cryptoHash,
|
||||
LookupResultArray* results);
|
||||
|
||||
private:
|
||||
// No subclassing
|
||||
~nsUrlClassifierDBServiceWorker();
|
||||
@ -128,8 +135,6 @@ private:
|
||||
// Disallow copy constructor
|
||||
nsUrlClassifierDBServiceWorker(nsUrlClassifierDBServiceWorker&);
|
||||
|
||||
nsresult OpenDb();
|
||||
|
||||
// Applies the current transaction and resets the update/working times.
|
||||
nsresult ApplyUpdate();
|
||||
|
||||
@ -149,6 +154,7 @@ private:
|
||||
uint32_t aCount,
|
||||
LookupResultArray& results);
|
||||
|
||||
// Can only be used on the background thread
|
||||
nsCOMPtr<nsICryptoHash> mCryptoHash;
|
||||
|
||||
nsAutoPtr<Classifier> mClassifier;
|
||||
@ -241,6 +247,76 @@ nsUrlClassifierDBServiceWorker::QueueLookup(const nsACString& spec,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
nsUrlClassifierDBServiceWorker::DoLocalLookup(const nsACString& spec,
|
||||
const nsACString& tables,
|
||||
nsICryptoHash* cryptoHash,
|
||||
LookupResultArray* results)
|
||||
{
|
||||
LOG(("nsUrlClassifierDBServiceWorker::DoLocalLookup %s (main=%s) %p",
|
||||
spec.Data(), NS_IsMainThread() ? "true" : "false", this));
|
||||
if (!results) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
// Bail if we haven't been initialized on the background thread.
|
||||
if (!mClassifier) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// We ignore failures from Check because we'd rather return the
|
||||
// results that were found than fail.
|
||||
mClassifier->Check(spec, tables, gFreshnessGuarantee, cryptoHash, *results);
|
||||
|
||||
LOG(("Found %d results.", results->Length()));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static nsresult
|
||||
TablesToResponse(const nsACString& tables,
|
||||
bool checkMalware,
|
||||
bool checkPhishing,
|
||||
bool checkTracking)
|
||||
{
|
||||
if (checkMalware &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-malware-"), tables)) {
|
||||
return NS_ERROR_MALWARE_URI;
|
||||
}
|
||||
if (checkPhishing &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-phish-"), tables)) {
|
||||
return NS_ERROR_PHISHING_URI;
|
||||
}
|
||||
if (checkTracking &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-track-"), tables)) {
|
||||
return NS_ERROR_TRACKING_URI;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static nsresult
|
||||
ProcessLookupResults(LookupResultArray* results,
|
||||
bool checkMalware,
|
||||
bool checkPhishing,
|
||||
bool checkTracking)
|
||||
{
|
||||
// Build a stringified list of result tables.
|
||||
nsTArray<nsCString> tables;
|
||||
for (uint32_t i = 0; i < results->Length(); i++) {
|
||||
LookupResult& result = results->ElementAt(i);
|
||||
MOZ_ASSERT(!result.mNoise, "Lookup results should not have noise added");
|
||||
LOG(("Found result from table %s", result.mTableName.get()));
|
||||
if (tables.IndexOf(result.mTableName) == nsTArray<nsCString>::NoIndex) {
|
||||
tables.AppendElement(result.mTableName);
|
||||
}
|
||||
}
|
||||
nsAutoCString tableStr;
|
||||
for (uint32_t i = 0; i < tables.Length(); i++) {
|
||||
if (i != 0)
|
||||
tableStr.Append(',');
|
||||
tableStr.Append(tables[i]);
|
||||
}
|
||||
return TablesToResponse(tableStr, checkMalware, checkPhishing, checkTracking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup up a key in the database is a two step process:
|
||||
*
|
||||
@ -262,13 +338,6 @@ nsUrlClassifierDBServiceWorker::DoLookup(const nsACString& spec,
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
nsresult rv = OpenDb();
|
||||
if (NS_FAILED(rv)) {
|
||||
c->LookupComplete(nullptr);
|
||||
NS_ERROR("Unable to open SafeBrowsing database.");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
#if defined(PR_LOGGING)
|
||||
PRIntervalTime clockStart = 0;
|
||||
if (LOG_ENABLED()) {
|
||||
@ -282,10 +351,11 @@ nsUrlClassifierDBServiceWorker::DoLookup(const nsACString& spec,
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// we ignore failures from Check because we'd rather return the
|
||||
// results that were found than fail.
|
||||
mClassifier->SetFreshTime(gFreshnessGuarantee);
|
||||
mClassifier->Check(spec, tables, *results);
|
||||
nsresult rv = DoLocalLookup(spec, tables, nullptr, results);
|
||||
if (NS_FAILED(rv)) {
|
||||
c->LookupComplete(nullptr);
|
||||
return rv;
|
||||
}
|
||||
|
||||
LOG(("Found %d results.", results->Length()));
|
||||
|
||||
@ -457,6 +527,7 @@ NS_IMETHODIMP
|
||||
nsUrlClassifierDBServiceWorker::BeginStream(const nsACString &table)
|
||||
{
|
||||
LOG(("nsUrlClassifierDBServiceWorker::BeginStream"));
|
||||
MOZ_ASSERT(!NS_IsMainThread(), "Streaming must be on the background thread");
|
||||
|
||||
if (gShuttingDownThread)
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
@ -732,13 +803,12 @@ nsUrlClassifierDBServiceWorker::CacheMisses(PrefixArray *results)
|
||||
nsresult
|
||||
nsUrlClassifierDBServiceWorker::OpenDb()
|
||||
{
|
||||
MOZ_ASSERT(!NS_IsMainThread(), "Must initialize DB on background thread");
|
||||
// Connection already open, don't do anything.
|
||||
if (mClassifier) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
LOG(("Opening db"));
|
||||
|
||||
nsresult rv;
|
||||
mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
@ -748,8 +818,6 @@ nsUrlClassifierDBServiceWorker::OpenDb()
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
classifier->SetFreshTime(gFreshnessGuarantee);
|
||||
|
||||
rv = classifier->Open(*mCacheDir);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
@ -1025,25 +1093,9 @@ NS_IMPL_ISUPPORTS(nsUrlClassifierClassifyCallback,
|
||||
NS_IMETHODIMP
|
||||
nsUrlClassifierClassifyCallback::HandleEvent(const nsACString& tables)
|
||||
{
|
||||
// XXX: we should probably have the wardens tell the service which table
|
||||
// names match with which classification. For now the table names give
|
||||
// enough information.
|
||||
nsresult response = NS_OK;
|
||||
|
||||
if (mCheckMalware &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-malware-"), tables)) {
|
||||
response = NS_ERROR_MALWARE_URI;
|
||||
} else if (mCheckPhishing &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-phish-"), tables)) {
|
||||
response = NS_ERROR_PHISHING_URI;
|
||||
} else if (mCheckTracking &&
|
||||
FindInReadable(NS_LITERAL_CSTRING("-track-"), tables)) {
|
||||
LOG(("Blocking tracking uri [this=%p]", this));
|
||||
response = NS_ERROR_TRACKING_URI;
|
||||
}
|
||||
|
||||
nsresult response = TablesToResponse(tables, mCheckMalware,
|
||||
mCheckPhishing, mCheckTracking);
|
||||
mCallback->OnClassifyComplete(response);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -1141,6 +1193,7 @@ nsUrlClassifierDBService::Init()
|
||||
if (!gUrlClassifierDbServiceLog)
|
||||
gUrlClassifierDbServiceLog = PR_NewLogModule("UrlClassifierDbService");
|
||||
#endif
|
||||
MOZ_ASSERT(NS_IsMainThread(), "Must initialize DB service on main thread");
|
||||
|
||||
// Retrieve all the preferences.
|
||||
mCheckMalware = Preferences::GetBool(CHECK_MALWARE_PREF,
|
||||
@ -1170,7 +1223,7 @@ nsUrlClassifierDBService::Init()
|
||||
|
||||
// Force PSM loading on main thread
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsICryptoHash> acryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
|
||||
mCryptoHashMain = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Directory providers must also be accessed on the main thread.
|
||||
@ -1199,6 +1252,10 @@ nsUrlClassifierDBService::Init()
|
||||
|
||||
// Proxy for calling the worker on the background thread
|
||||
mWorkerProxy = new UrlClassifierDBServiceWorkerProxy(mWorker);
|
||||
rv = mWorkerProxy->OpenDb();
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Add an observer for shutdown
|
||||
nsCOMPtr<nsIObserverService> observerService =
|
||||
@ -1212,6 +1269,28 @@ nsUrlClassifierDBService::Init()
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static void BuildTables(bool aTrackingProtectionEnabled, nsCString &tables)
|
||||
{
|
||||
nsAutoCString malware;
|
||||
// LookupURI takes a comma-separated list already.
|
||||
Preferences::GetCString(MALWARE_TABLE_PREF, &malware);
|
||||
if (!malware.IsEmpty()) {
|
||||
tables.Append(malware);
|
||||
}
|
||||
nsAutoCString phishing;
|
||||
Preferences::GetCString(PHISH_TABLE_PREF, &phishing);
|
||||
if (!phishing.IsEmpty()) {
|
||||
tables.Append(',');
|
||||
tables.Append(phishing);
|
||||
}
|
||||
nsAutoCString tracking;
|
||||
Preferences::GetCString(TRACKING_TABLE_PREF, &tracking);
|
||||
if (aTrackingProtectionEnabled && !tracking.IsEmpty()) {
|
||||
tables.Append(',');
|
||||
tables.Append(tracking);
|
||||
}
|
||||
}
|
||||
|
||||
// nsChannelClassifier is the only consumer of this interface.
|
||||
NS_IMETHODIMP
|
||||
nsUrlClassifierDBService::Classify(nsIPrincipal* aPrincipal,
|
||||
@ -1233,25 +1312,8 @@ nsUrlClassifierDBService::Classify(nsIPrincipal* aPrincipal,
|
||||
if (!callback) return NS_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
nsAutoCString tables;
|
||||
nsAutoCString malware;
|
||||
// LookupURI takes a comma-separated list already.
|
||||
Preferences::GetCString(MALWARE_TABLE_PREF, &malware);
|
||||
if (!malware.IsEmpty()) {
|
||||
tables.Append(malware);
|
||||
}
|
||||
nsAutoCString phishing;
|
||||
Preferences::GetCString(PHISH_TABLE_PREF, &phishing);
|
||||
if (!phishing.IsEmpty()) {
|
||||
tables.Append(',');
|
||||
tables.Append(phishing);
|
||||
}
|
||||
nsAutoCString tracking;
|
||||
Preferences::GetCString(TRACKING_TABLE_PREF, &tracking);
|
||||
if (aTrackingProtectionEnabled && !tracking.IsEmpty()) {
|
||||
LOG(("Looking up third party in tracking table, [cb=%p]", callback.get()));
|
||||
tables.Append(',');
|
||||
tables.Append(tracking);
|
||||
}
|
||||
BuildTables(aTrackingProtectionEnabled, tables);
|
||||
|
||||
nsresult rv = LookupURI(aPrincipal, tables, callback, false, result);
|
||||
if (rv == NS_ERROR_MALFORMED_URI) {
|
||||
*result = false;
|
||||
@ -1263,6 +1325,47 @@ nsUrlClassifierDBService::Classify(nsIPrincipal* aPrincipal,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsUrlClassifierDBService::ClassifyLocal(nsIPrincipal* aPrincipal,
|
||||
bool aTrackingProtectionEnabled,
|
||||
nsresult* aResponse)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread(), "ClassifyLocal must be on main thread");
|
||||
*aResponse = NS_OK;
|
||||
nsAutoCString tables;
|
||||
BuildTables(aTrackingProtectionEnabled, tables);
|
||||
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
|
||||
|
||||
uri = NS_GetInnermostURI(uri);
|
||||
NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
|
||||
|
||||
nsAutoCString key;
|
||||
// Canonicalize the url
|
||||
nsCOMPtr<nsIUrlClassifierUtils> utilsService =
|
||||
do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
|
||||
rv = utilsService->GetKeyForURI(uri, key);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
nsAutoPtr<LookupResultArray> results(new LookupResultArray());
|
||||
if (!results) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// We don't use the proxy, since this is a blocking lookup. In unittests, we
|
||||
// may not have been initalized, so don't crash.
|
||||
rv = mWorker->DoLocalLookup(key, tables, mCryptoHashMain, results);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
rv = ProcessLookupResults(results, mCheckMalware, mCheckPhishing,
|
||||
mCheckTracking);
|
||||
*aResponse = rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsUrlClassifierDBService::Lookup(nsIPrincipal* aPrincipal,
|
||||
const nsACString& tables,
|
||||
|
@ -33,6 +33,7 @@
|
||||
#define COMPLETE_LENGTH 32
|
||||
|
||||
class nsUrlClassifierDBServiceWorker;
|
||||
class nsICryptoHash;
|
||||
class nsIThread;
|
||||
class nsIURI;
|
||||
|
||||
@ -117,6 +118,10 @@ private:
|
||||
|
||||
// Thread that we do the updates on.
|
||||
static nsIThread* gDbBackgroundThread;
|
||||
|
||||
// nsICryptoHash for doing hash operations on the main thread. This is only
|
||||
// used for nsIURIClassifier.ClassifyLocal
|
||||
nsCOMPtr<nsICryptoHash> mCryptoHashMain;
|
||||
};
|
||||
|
||||
NS_DEFINE_STATIC_IID_ACCESSOR(nsUrlClassifierDBService, NS_URLCLASSIFIERDBSERVICE_CID)
|
||||
|
@ -143,6 +143,15 @@ UrlClassifierDBServiceWorkerProxy::ResetDatabase()
|
||||
return DispatchToWorkerThread(r);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
UrlClassifierDBServiceWorkerProxy::OpenDb()
|
||||
{
|
||||
nsCOMPtr<nsIRunnable> r =
|
||||
NS_NewRunnableMethod(mTarget,
|
||||
&nsIUrlClassifierDBServiceWorker::OpenDb);
|
||||
return DispatchToWorkerThread(r);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
UrlClassifierDBServiceWorkerProxy::CloseDb()
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user