1. Pass connect arg to mojom
2. Implement IsConnected 3. Move CallMethodOfObject and GetProperty to helper
This commit is contained in:
@@ -14,7 +14,9 @@ namespace brave_wallet {
|
||||
SolanaProviderImpl::SolanaProviderImpl() = default;
|
||||
SolanaProviderImpl::~SolanaProviderImpl() = default;
|
||||
|
||||
void SolanaProviderImpl::Connect(ConnectCallback callback) {
|
||||
void SolanaProviderImpl::Connect(absl::optional<base::Value> arg,
|
||||
ConnectCallback callback) {
|
||||
// TODO(darkdh): handle onlyIfTrusted when it exists
|
||||
// NOTIMPLEMENTED();
|
||||
// std::move(callback).Run(mojom::SolanaProviderError::kInternalError, "",
|
||||
// "");
|
||||
@@ -26,4 +28,9 @@ void SolanaProviderImpl::Disconnect() {
|
||||
NOTIMPLEMENTED();
|
||||
}
|
||||
|
||||
void SolanaProviderImpl::IsConnected(IsConnectedCallback callback) {
|
||||
NOTIMPLEMENTED();
|
||||
std::move(callback).Run(false);
|
||||
}
|
||||
|
||||
} // namespace brave_wallet
|
||||
|
||||
@@ -17,8 +17,10 @@ class SolanaProviderImpl final : public mojom::SolanaProvider {
|
||||
SolanaProviderImpl(const SolanaProviderImpl&) = delete;
|
||||
SolanaProviderImpl& operator=(const SolanaProviderImpl&) = delete;
|
||||
|
||||
void Connect(ConnectCallback callback) override;
|
||||
void Connect(absl::optional<base::Value> arg,
|
||||
ConnectCallback callback) override;
|
||||
void Disconnect() override;
|
||||
void IsConnected(IsConnectedCallback callback) override;
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
@@ -123,8 +123,13 @@ interface BraveWalletProvider {
|
||||
};
|
||||
|
||||
interface SolanaProvider {
|
||||
Connect() => (SolanaProviderError error, string message, string public_key);
|
||||
// Optional {onlyIfTrusted: true}, when that flag is enable, we will only
|
||||
// connect and emit a connect event if the application is trusted.
|
||||
Connect(mojo_base.mojom.DictionaryValue? arg)
|
||||
=> (SolanaProviderError error, string message, string public_key);
|
||||
Disconnect();
|
||||
[Sync]
|
||||
IsConnected() => (bool is_connected);
|
||||
// SignTransaction
|
||||
// SignAllTransactions
|
||||
// SignMessage, hex or utf8 encoding
|
||||
|
||||
@@ -4,6 +4,8 @@ source_set("renderer") {
|
||||
"brave_wallet_js_handler.h",
|
||||
"js_solana_provider.cc",
|
||||
"js_solana_provider.h",
|
||||
"v8_helper.cc",
|
||||
"v8_helper.h",
|
||||
]
|
||||
|
||||
deps = [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "brave/components/brave_wallet/common/hex_utils.h"
|
||||
#include "brave/components/brave_wallet/common/value_conversion_utils.h"
|
||||
#include "brave/components/brave_wallet/common/web3_provider_constants.h"
|
||||
#include "brave/components/brave_wallet/renderer/v8_helper.h"
|
||||
#include "brave/components/brave_wallet/resources/grit/brave_wallet_script_generated.h"
|
||||
#include "content/public/renderer/render_frame.h"
|
||||
#include "content/public/renderer/v8_value_converter.h"
|
||||
@@ -55,55 +56,6 @@ std::string LoadDataResource(const int id) {
|
||||
return std::string(resource_bundle.GetRawDataResource(id));
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Value> GetProperty(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Value> object,
|
||||
const std::u16string& name) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::Local<v8::String> name_str =
|
||||
gin::ConvertToV8(isolate, name).As<v8::String>();
|
||||
v8::Local<v8::Object> object_obj;
|
||||
if (!object->ToObject(context).ToLocal(&object_obj)) {
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
return object_obj->Get(context, name_str);
|
||||
}
|
||||
|
||||
void CallMethodOfObject(blink::WebLocalFrame* web_frame,
|
||||
const std::u16string& object_name,
|
||||
const std::u16string& method_name,
|
||||
base::Value arguments) {
|
||||
if (web_frame->IsProvisional())
|
||||
return;
|
||||
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
||||
v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
|
||||
v8::Context::Scope context_scope(context);
|
||||
v8::MicrotasksScope microtasks(v8::Isolate::GetCurrent(),
|
||||
v8::MicrotasksScope::kDoNotRunMicrotasks);
|
||||
v8::Local<v8::Value> object;
|
||||
v8::Local<v8::Value> method;
|
||||
if (!GetProperty(context, context->Global(), object_name).ToLocal(&object) ||
|
||||
!GetProperty(context, object, method_name).ToLocal(&method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Without the IsFunction test here JS blocking from content settings
|
||||
// will trigger a DCHECK crash.
|
||||
if (method.IsEmpty() || !method->IsFunction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
for (auto const& argument : arguments.GetList()) {
|
||||
args.push_back(
|
||||
content::V8ValueConverter::Create()->ToV8Value(&argument, context));
|
||||
}
|
||||
|
||||
web_frame->ExecuteMethodAndReturnValue(v8::Local<v8::Function>::Cast(method),
|
||||
object, static_cast<int>(args.size()),
|
||||
args.data());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace brave_wallet {
|
||||
@@ -616,9 +568,18 @@ void BraveWalletJSHandler::InjectInitScript() {
|
||||
|
||||
void BraveWalletJSHandler::FireEvent(const std::string& event,
|
||||
base::Value event_args) {
|
||||
base::Value args = base::Value(base::Value::Type::LIST);
|
||||
args.Append(event);
|
||||
args.Append(std::move(event_args));
|
||||
base::Value args_list = base::Value(base::Value::Type::LIST);
|
||||
args_list.Append(event);
|
||||
args_list.Append(std::move(event_args));
|
||||
|
||||
v8::Isolate* isolate = v8::Isolate::GetCurrent();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
v8::Local<v8::Context> context =
|
||||
render_frame_->GetWebFrame()->MainWorldScriptContext();
|
||||
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
args.push_back(
|
||||
content::V8ValueConverter::Create()->ToV8Value(&args_list, context));
|
||||
CallMethodOfObject(render_frame_->GetWebFrame(), u"ethereum", u"emit",
|
||||
std::move(args));
|
||||
}
|
||||
|
||||
@@ -7,71 +7,25 @@
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/notreached.h"
|
||||
#include "brave/components/brave_wallet/common/brave_wallet_response_helpers.h"
|
||||
#include "brave/components/brave_wallet/common/web3_provider_constants.h"
|
||||
#include "brave/components/brave_wallet/renderer/v8_helper.h"
|
||||
#include "content/public/renderer/v8_value_converter.h"
|
||||
#include "gin/handle.h"
|
||||
#include "gin/object_template_builder.h"
|
||||
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
|
||||
#include "third_party/blink/public/web/web_local_frame.h"
|
||||
#include "v8/include/v8-function.h"
|
||||
#include "v8/include/v8-microtask-queue.h"
|
||||
|
||||
namespace brave_wallet {
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO: move these to helper
|
||||
v8::MaybeLocal<v8::Value> GetProperty(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Value> object,
|
||||
const std::u16string& name) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::Local<v8::String> name_str =
|
||||
gin::ConvertToV8(isolate, name).As<v8::String>();
|
||||
v8::Local<v8::Object> object_obj;
|
||||
if (!object->ToObject(context).ToLocal(&object_obj)) {
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
return object_obj->Get(context, name_str);
|
||||
}
|
||||
v8::MaybeLocal<v8::Value> CallMethodOfObject(
|
||||
blink::WebLocalFrame* web_frame,
|
||||
const std::u16string& object_name,
|
||||
const std::u16string& method_name,
|
||||
std::vector<v8::Local<v8::Value>>&& args) {
|
||||
if (web_frame->IsProvisional())
|
||||
return v8::Local<v8::Value>();
|
||||
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
||||
v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
|
||||
v8::Context::Scope context_scope(context);
|
||||
v8::MicrotasksScope microtasks(v8::Isolate::GetCurrent(),
|
||||
v8::MicrotasksScope::kDoNotRunMicrotasks);
|
||||
v8::Local<v8::Value> object;
|
||||
v8::Local<v8::Value> method;
|
||||
if (!GetProperty(context, context->Global(), object_name).ToLocal(&object) ||
|
||||
!GetProperty(context, object, method_name).ToLocal(&method)) {
|
||||
return v8::Local<v8::Value>();
|
||||
}
|
||||
|
||||
// Without the IsFunction test here JS blocking from content settings
|
||||
// will trigger a DCHECK crash.
|
||||
if (method.IsEmpty() || !method->IsFunction()) {
|
||||
return v8::Local<v8::Value>();
|
||||
}
|
||||
|
||||
return web_frame->CallFunctionEvenIfScriptDisabled(
|
||||
v8::Local<v8::Function>::Cast(method), object,
|
||||
static_cast<int>(args.size()), args.data());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
JSSolanaProvider::JSSolanaProvider(bool use_native_wallet,
|
||||
content::RenderFrame* render_frame)
|
||||
: use_native_wallet_(use_native_wallet), render_frame_(render_frame) {
|
||||
: use_native_wallet_(use_native_wallet),
|
||||
render_frame_(render_frame),
|
||||
v8_value_converter_(content::V8ValueConverter::Create()) {
|
||||
EnsureConnected();
|
||||
}
|
||||
JSSolanaProvider::~JSSolanaProvider() = default;
|
||||
@@ -138,12 +92,14 @@ bool JSSolanaProvider::GetIsPhantom(gin::Arguments* arguments) {
|
||||
}
|
||||
|
||||
bool JSSolanaProvider::GetIsConnected(gin::Arguments* arguments) {
|
||||
NOTIMPLEMENTED();
|
||||
return false;
|
||||
bool is_connected = false;
|
||||
if (!solana_provider_->IsConnected(&is_connected)) {
|
||||
return false;
|
||||
}
|
||||
return is_connected;
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> JSSolanaProvider::Connect(gin::Arguments* arguments) {
|
||||
// TODO: support onlyIfTrusted
|
||||
if (!EnsureConnected())
|
||||
return v8::Local<v8::Promise>();
|
||||
|
||||
@@ -154,13 +110,33 @@ v8::Local<v8::Promise> JSSolanaProvider::Connect(gin::Arguments* arguments) {
|
||||
return v8::Local<v8::Promise>();
|
||||
}
|
||||
|
||||
// Get base::Value arg to pass
|
||||
absl::optional<base::Value> arg = absl::nullopt;
|
||||
v8::Local<v8::Value> v8_arg;
|
||||
if (arguments->Length() > 1 ||
|
||||
(arguments->Length() == 1 && !arguments->GetNext(&v8_arg))) {
|
||||
arguments->ThrowError();
|
||||
return v8::Local<v8::Promise>();
|
||||
}
|
||||
if (!v8_arg.IsEmpty()) {
|
||||
std::unique_ptr<base::Value> arg_value =
|
||||
v8_value_converter_->FromV8Value(v8_arg, isolate->GetCurrentContext());
|
||||
if (!arg_value || !arg_value->is_dict()) {
|
||||
arguments->ThrowError();
|
||||
return v8::Local<v8::Promise>();
|
||||
}
|
||||
arg = std::move(*arg_value);
|
||||
}
|
||||
|
||||
auto global_context(
|
||||
v8::Global<v8::Context>(isolate, isolate->GetCurrentContext()));
|
||||
auto promise_resolver(
|
||||
v8::Global<v8::Promise::Resolver>(isolate, resolver.ToLocalChecked()));
|
||||
solana_provider_->Connect(base::BindOnce(
|
||||
&JSSolanaProvider::OnConnect, weak_ptr_factory_.GetWeakPtr(),
|
||||
std::move(global_context), std::move(promise_resolver), isolate));
|
||||
solana_provider_->Connect(
|
||||
std::move(arg),
|
||||
base::BindOnce(&JSSolanaProvider::OnConnect,
|
||||
weak_ptr_factory_.GetWeakPtr(), std::move(global_context),
|
||||
std::move(promise_resolver), isolate));
|
||||
|
||||
return resolver.ToLocalChecked()->GetPromise();
|
||||
}
|
||||
@@ -178,13 +154,7 @@ v8::Local<v8::Promise> JSSolanaProvider::Disconnect(gin::Arguments* arguments) {
|
||||
std::ignore = resolver.ToLocalChecked()->Resolve(isolate->GetCurrentContext(),
|
||||
v8::Undefined(isolate));
|
||||
|
||||
// emit disconnect
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
const base::Value event("disconnect");
|
||||
args.push_back(content::V8ValueConverter::Create()->ToV8Value(
|
||||
&event, isolate->GetCurrentContext()));
|
||||
CallMethodOfObject(render_frame_->GetWebFrame(), u"solana", u"emit",
|
||||
std::move(args));
|
||||
FireEvent(kDisconnectEvent, std::vector<v8::Local<v8::Value>>());
|
||||
return resolver.ToLocalChecked()->GetPromise();
|
||||
}
|
||||
|
||||
@@ -219,6 +189,19 @@ v8::Local<v8::Promise> JSSolanaProvider::SignAllTransaction(
|
||||
return v8::Local<v8::Promise>();
|
||||
}
|
||||
|
||||
void JSSolanaProvider::FireEvent(
|
||||
const std::string& event,
|
||||
std::vector<v8::Local<v8::Value>>&& event_args) {
|
||||
v8::Local<v8::Context> context =
|
||||
render_frame_->GetWebFrame()->MainWorldScriptContext();
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
const base::Value event_value(event);
|
||||
args.push_back(v8_value_converter_->ToV8Value(&event_value, context));
|
||||
args.insert(args.end(), event_args.begin(), event_args.end());
|
||||
CallMethodOfObject(render_frame_->GetWebFrame(), u"solana", u"emit",
|
||||
std::move(args));
|
||||
}
|
||||
|
||||
void JSSolanaProvider::OnConnect(
|
||||
v8::Global<v8::Context> global_context,
|
||||
v8::Global<v8::Promise::Resolver> promise_resolver,
|
||||
@@ -232,13 +215,11 @@ void JSSolanaProvider::OnConnect(
|
||||
v8::Local<v8::Context> context = global_context.Get(isolate);
|
||||
v8::Local<v8::Value> result;
|
||||
v8::Local<v8::Value> v8_public_key;
|
||||
std::unique_ptr<content::V8ValueConverter> v8_value_converter =
|
||||
content::V8ValueConverter::Create();
|
||||
if (error == mojom::SolanaProviderError::kSuccess) {
|
||||
// use @solana/web3.js and create publicKey from base58 string
|
||||
const base::Value public_key_value(public_key);
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
args.push_back(v8_value_converter->ToV8Value(&public_key_value, context));
|
||||
args.push_back(v8_value_converter_->ToV8Value(&public_key_value, context));
|
||||
|
||||
v8::MaybeLocal<v8::Value> public_key_result =
|
||||
CallMethodOfObject(render_frame_->GetWebFrame(), u"solana",
|
||||
@@ -249,19 +230,16 @@ void JSSolanaProvider::OnConnect(
|
||||
} else {
|
||||
std::unique_ptr<base::Value> formed_response =
|
||||
GetProviderErrorDictionary(error, error_message);
|
||||
result = v8_value_converter->ToV8Value(formed_response.get(), context);
|
||||
result = v8_value_converter_->ToV8Value(formed_response.get(), context);
|
||||
}
|
||||
|
||||
SendResponse(std::move(global_context), std::move(promise_resolver), isolate,
|
||||
std::move(result),
|
||||
error == mojom::SolanaProviderError::kSuccess);
|
||||
// emit connect
|
||||
|
||||
std::vector<v8::Local<v8::Value>> args;
|
||||
const base::Value event("connect");
|
||||
args.push_back(v8_value_converter->ToV8Value(&event, context));
|
||||
args.push_back(std::move(v8_public_key));
|
||||
CallMethodOfObject(render_frame_->GetWebFrame(), u"solana", u"emit",
|
||||
std::move(args));
|
||||
FireEvent(kConnectEvent, std::move(args));
|
||||
}
|
||||
|
||||
void JSSolanaProvider::SendResponse(
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "brave/components/brave_wallet/common/brave_wallet.mojom.h"
|
||||
#include "content/public/renderer/render_frame.h"
|
||||
#include "content/public/renderer/v8_value_converter.h"
|
||||
#include "gin/arguments.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
@@ -56,6 +58,8 @@ class JSSolanaProvider final : public gin::Wrappable<JSSolanaProvider> {
|
||||
v8::Local<v8::Promise> SignAllTransaction(gin::Arguments* arguments);
|
||||
|
||||
// TODO: fire accountChanged event
|
||||
void FireEvent(const std::string& event,
|
||||
std::vector<v8::Local<v8::Value>>&& event_args);
|
||||
|
||||
void OnConnect(v8::Global<v8::Context> global_context,
|
||||
v8::Global<v8::Promise::Resolver> promise_resolver,
|
||||
@@ -72,6 +76,7 @@ class JSSolanaProvider final : public gin::Wrappable<JSSolanaProvider> {
|
||||
|
||||
bool use_native_wallet_ = false;
|
||||
raw_ptr<content::RenderFrame> render_frame_ = nullptr;
|
||||
std::unique_ptr<content::V8ValueConverter> v8_value_converter_;
|
||||
mojo::Remote<mojom::SolanaProvider> solana_provider_;
|
||||
base::WeakPtrFactory<JSSolanaProvider> weak_ptr_factory_{this};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* Copyright (c) 2022 The Brave Authors. All rights reserved.
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "brave/components/brave_wallet/renderer/v8_helper.h"
|
||||
|
||||
#include "gin/converter.h"
|
||||
#include "third_party/blink/public/web/web_local_frame.h"
|
||||
#include "v8/include/v8-function.h"
|
||||
#include "v8/include/v8-microtask-queue.h"
|
||||
|
||||
namespace brave_wallet {
|
||||
|
||||
v8::MaybeLocal<v8::Value> GetProperty(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Value> object,
|
||||
const std::u16string& name) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::Local<v8::String> name_str =
|
||||
gin::ConvertToV8(isolate, name).As<v8::String>();
|
||||
v8::Local<v8::Object> object_obj;
|
||||
if (!object->ToObject(context).ToLocal(&object_obj)) {
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
return object_obj->Get(context, name_str);
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Value> CallMethodOfObject(
|
||||
blink::WebLocalFrame* web_frame,
|
||||
const std::u16string& object_name,
|
||||
const std::u16string& method_name,
|
||||
std::vector<v8::Local<v8::Value>>&& args) {
|
||||
if (web_frame->IsProvisional())
|
||||
return v8::Local<v8::Value>();
|
||||
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
||||
v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
|
||||
v8::Context::Scope context_scope(context);
|
||||
v8::MicrotasksScope microtasks(v8::Isolate::GetCurrent(),
|
||||
v8::MicrotasksScope::kDoNotRunMicrotasks);
|
||||
v8::Local<v8::Value> object;
|
||||
v8::Local<v8::Value> method;
|
||||
if (!GetProperty(context, context->Global(), object_name).ToLocal(&object) ||
|
||||
!GetProperty(context, object, method_name).ToLocal(&method)) {
|
||||
return v8::Local<v8::Value>();
|
||||
}
|
||||
|
||||
// Without the IsFunction test here JS blocking from content settings
|
||||
// will trigger a DCHECK crash.
|
||||
if (method.IsEmpty() || !method->IsFunction()) {
|
||||
return v8::Local<v8::Value>();
|
||||
}
|
||||
|
||||
return web_frame->CallFunctionEvenIfScriptDisabled(
|
||||
v8::Local<v8::Function>::Cast(method), object,
|
||||
static_cast<int>(args.size()), args.data());
|
||||
}
|
||||
|
||||
} // namespace brave_wallet
|
||||
@@ -0,0 +1,34 @@
|
||||
/* Copyright (c) 2022 The Brave Authors. All rights reserved.
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_RENDERER_V8_HELPER_H_
|
||||
#define BRAVE_COMPONENTS_BRAVE_WALLET_RENDERER_V8_HELPER_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "v8/include/v8-context.h"
|
||||
#include "v8/include/v8-local-handle.h"
|
||||
#include "v8/include/v8-value.h"
|
||||
|
||||
namespace blink {
|
||||
class WebLocalFrame;
|
||||
} // namespace blink
|
||||
|
||||
namespace brave_wallet {
|
||||
|
||||
v8::MaybeLocal<v8::Value> GetProperty(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Value> object,
|
||||
const std::u16string& name);
|
||||
|
||||
v8::MaybeLocal<v8::Value> CallMethodOfObject(
|
||||
blink::WebLocalFrame* web_frame,
|
||||
const std::u16string& object_name,
|
||||
const std::u16string& method_name,
|
||||
std::vector<v8::Local<v8::Value>>&& args);
|
||||
|
||||
} // namespace brave_wallet
|
||||
|
||||
#endif // BRAVE_COMPONENTS_BRAVE_WALLET_RENDERER_V8_HELPER_H_
|
||||
Reference in New Issue
Block a user