Commit 0297659d authored by Victor Fei's avatar Victor Fei Committed by Commit Bot

Stablizing Mac autofill accessibility when popup show/hides

This is a follow up change of CL:1667787, which introduced
AutofillPopupControllerImpl::FireControlsChangedEvent for autofill
popup accessibility.

On Mac and potentially other platforms, when accessibility is
enabled, upon invoking autofill popup and FireControlsChangedEvent
we end up dereferencing a nullptr of AxPlatformNode due to
Mac does not have a complete implementation of AxPlatformNode yet.

This CL fixes the above by adding a check for AxPlatformNode in
FireControlsChangedEvent.

Changes:
 1. Introduced a check for invalid AxPlatformNode in
    AutofillPopupControllerImpl::FireControlsChangedEvent to return
    early from firing event.
 2. Added associated unit tests for FireControlsChangedEvent.
 3. Exposed GetWebContentsPopupControllerAxId virtual to
    AutofillPopupDelegate..

Bug: 986587
Change-Id: I9a6ed49330a9ea9d7d4e5483c3ee06d5675919ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1714297
Commit-Queue: Victor Fei <vicfei@microsoft.com>
Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarIan Prest <iapres@microsoft.com>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#686461}
parent b6df81a7
......@@ -597,24 +597,31 @@ void AutofillPopupControllerImpl::FireControlsChangedEvent(bool is_show) {
// Retrieve the ax node id associated with the current web contents' element
// that has a controller relation to the current autofill popup.
int32_t node_id = static_cast<AutofillExternalDelegate*>(delegate_.get())
->GetWebContentsPopupControllerAxId();
int32_t node_id = delegate_->GetWebContentsPopupControllerAxId();
// We can only raise controls changed accessibility event when we have a valid
// ax tree and an ax node associated with the ax tree for the popup
// controller, and a valid ax unique id for the popup controllee.
if (!ax_tree_manager || !ax_tree_manager->GetDelegate(tree_id, node_id) ||
!view_->GetAxUniqueId())
if (!ax_tree_manager)
return;
ui::AXPlatformNodeDelegate* ax_platform_node_delegate =
ax_tree_manager->GetDelegate(tree_id, node_id);
if (!ax_platform_node_delegate)
return;
ui::AXPlatformNode* target_node =
ax_platform_node_delegate->GetFromNodeID(node_id);
base::Optional<int32_t> popup_ax_id = view_->GetAxUniqueId();
if (!target_node || !popup_ax_id)
return;
// All the conditions are valid, raise the accessibility event and set global
// popup ax unique id.
if (is_show)
ui::SetActivePopupAxUniqueId(view_->GetAxUniqueId());
ui::SetActivePopupAxUniqueId(popup_ax_id);
else
ui::ClearActivePopupAxUniqueId();
ax_tree_manager->GetDelegate(tree_id, node_id)
->GetFromNodeID(node_id)
->NotifyAccessibilityEvent(ax::mojom::Event::kControlsChanged);
target_node->NotifyAccessibilityEvent(ax::mojom::Event::kControlsChanged);
}
} // namespace autofill
......@@ -139,6 +139,11 @@ class AutofillPopupControllerImpl : public AutofillPopupController {
AutofillPopupLayoutModel& LayoutModelForTesting() { return layout_model_; }
// Raise an accessibility event to indicate the controls relation of the
// form control of the popup and popup itself has changed based on the popup's
// show or hide action.
void FireControlsChangedEvent(bool is_show);
private:
#if !defined(OS_ANDROID)
FRIEND_TEST_ALL_PREFIXES(AutofillPopupControllerUnitTest, ElideText);
......@@ -155,12 +160,8 @@ class AutofillPopupControllerImpl : public AutofillPopupController {
// Hides |view_| unless it is null and then deletes |this|.
void HideViewAndDie();
// Raise an accessibility event to indicate the controls relation of the
// form control of the popup and popup itself has changed based on the popup's
// show or hide action.
void FireControlsChangedEvent(bool is_show);
friend class AutofillPopupControllerUnitTest;
friend class AutofillPopupControllerAccessibilityUnitTest;
void SetViewForTesting(AutofillPopupView* view) { view_ = view; }
PopupControllerCommon controller_common_;
......
......@@ -11,6 +11,7 @@
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/accessibility/accessibility_state_utils.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
#include "chrome/browser/ui/autofill/autofill_popup_view.h"
#include "chrome/browser/ui/autofill/popup_view_common.h"
......@@ -23,14 +24,26 @@
#include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/autofill_test_utils.h"
#include "components/autofill/core/browser/test_autofill_client.h"
#include "components/autofill/core/browser/test_autofill_driver.h"
#include "components/autofill/core/browser/ui/popup_item_ids.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_active_popup.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/accessibility/ax_tree_manager_map.h"
#include "ui/accessibility/platform/ax_platform_node_base.h"
#include "ui/accessibility/platform/ax_platform_node_delegate_base.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/text_utils.h"
#if !defined(OS_CHROMEOS)
#include "content/public/browser/browser_accessibility_state.h"
#endif
using ::testing::_;
using ::testing::AtLeast;
using ::testing::Mock;
......@@ -42,12 +55,48 @@ using base::WeakPtr;
namespace autofill {
namespace {
class MockAutofillDriver : public TestAutofillDriver {
public:
MockAutofillDriver() = default;
~MockAutofillDriver() override = default;
MOCK_CONST_METHOD0(GetAxTreeId, ui::AXTreeID());
private:
DISALLOW_COPY_AND_ASSIGN(MockAutofillDriver);
};
class MockAutofillClient : public autofill::TestAutofillClient {
public:
MockAutofillClient() : prefs_(autofill::test::PrefServiceForTesting()) {}
~MockAutofillClient() override = default;
PrefService* GetPrefs() override { return prefs_.get(); }
private:
std::unique_ptr<PrefService> prefs_;
DISALLOW_COPY_AND_ASSIGN(MockAutofillClient);
};
class MockAutofillManager : public AutofillManager {
public:
MockAutofillManager(AutofillDriver* driver, MockAutofillClient* client)
: AutofillManager(driver,
client,
client->GetPersonalDataManager(),
client->GetAutocompleteHistoryManager()) {}
~MockAutofillManager() override = default;
private:
DISALLOW_COPY_AND_ASSIGN(MockAutofillManager);
};
class MockAutofillExternalDelegate : public AutofillExternalDelegate {
public:
MockAutofillExternalDelegate(AutofillManager* autofill_manager,
AutofillDriver* autofill_driver)
: AutofillExternalDelegate(autofill_manager, autofill_driver) {}
~MockAutofillExternalDelegate() override {}
~MockAutofillExternalDelegate() override = default;
void DidSelectSuggestion(const base::string16& value,
int identifier) override {}
......@@ -62,22 +111,10 @@ class MockAutofillExternalDelegate : public AutofillExternalDelegate {
MOCK_METHOD0(OnPopupSuppressed, void());
};
class MockAutofillClient : public autofill::TestAutofillClient {
public:
MockAutofillClient() : prefs_(autofill::test::PrefServiceForTesting()) {}
~MockAutofillClient() override {}
PrefService* GetPrefs() override { return prefs_.get(); }
private:
std::unique_ptr<PrefService> prefs_;
DISALLOW_COPY_AND_ASSIGN(MockAutofillClient);
};
class MockAutofillPopupView : public AutofillPopupView {
public:
MockAutofillPopupView() {}
MockAutofillPopupView() = default;
~MockAutofillPopupView() override = default;
MOCK_METHOD0(Show, void());
MOCK_METHOD0(Hide, void());
......@@ -104,22 +141,23 @@ class TestAutofillPopupController : public AutofillPopupControllerImpl {
LayoutModelForTesting().SetUpForTesting(
std::make_unique<MockPopupViewCommonForUnitTesting>());
}
~TestAutofillPopupController() override {}
~TestAutofillPopupController() override = default;
// Making protected functions public for testing
using AutofillPopupControllerImpl::element_bounds;
using AutofillPopupControllerImpl::FireControlsChangedEvent;
using AutofillPopupControllerImpl::GetElidedLabelAt;
using AutofillPopupControllerImpl::GetElidedValueAt;
using AutofillPopupControllerImpl::GetLineCount;
using AutofillPopupControllerImpl::GetSuggestionAt;
using AutofillPopupControllerImpl::GetElidedValueAt;
using AutofillPopupControllerImpl::GetElidedLabelAt;
using AutofillPopupControllerImpl::GetWeakPtr;
using AutofillPopupControllerImpl::popup_bounds;
using AutofillPopupControllerImpl::RemoveSelectedLine;
using AutofillPopupControllerImpl::selected_line;
using AutofillPopupControllerImpl::SetSelectedLine;
using AutofillPopupControllerImpl::SelectNextLine;
using AutofillPopupControllerImpl::SelectPreviousLine;
using AutofillPopupControllerImpl::RemoveSelectedLine;
using AutofillPopupControllerImpl::popup_bounds;
using AutofillPopupControllerImpl::element_bounds;
using AutofillPopupControllerImpl::SetSelectedLine;
using AutofillPopupControllerImpl::SetValues;
using AutofillPopupControllerImpl::GetWeakPtr;
MOCK_METHOD0(OnSuggestionsChanged, void());
MOCK_METHOD0(Hide, void());
......@@ -128,6 +166,39 @@ class TestAutofillPopupController : public AutofillPopupControllerImpl {
}
};
class MockAxTreeManager : public ui::AXTreeManager {
public:
MockAxTreeManager() = default;
~MockAxTreeManager() = default;
MOCK_CONST_METHOD2(GetNodeFromTree,
ui::AXNode*(const ui::AXTreeID tree_id,
const int32_t node_id));
MOCK_CONST_METHOD2(GetDelegate,
ui::AXPlatformNodeDelegate*(const ui::AXTreeID tree_id,
const int32_t node_id));
MOCK_CONST_METHOD1(GetRootDelegate,
ui::AXPlatformNodeDelegate*(const ui::AXTreeID tree_id));
MOCK_CONST_METHOD0(GetTreeID, ui::AXTreeID());
MOCK_CONST_METHOD0(GetParentTreeID, ui::AXTreeID());
MOCK_CONST_METHOD0(GetRootAsAXNode, ui::AXNode*());
MOCK_CONST_METHOD0(GetParentNodeFromParentTreeAsAXNode, ui::AXNode*());
private:
DISALLOW_COPY_AND_ASSIGN(MockAxTreeManager);
};
class MockAxPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase {
public:
MockAxPlatformNodeDelegate() = default;
~MockAxPlatformNodeDelegate() override = default;
MOCK_METHOD1(GetFromNodeID, ui::AXPlatformNode*(int32_t id));
private:
DISALLOW_COPY_AND_ASSIGN(MockAxPlatformNodeDelegate);
};
static constexpr base::Optional<int> kNoSelection;
} // namespace
......@@ -137,25 +208,12 @@ class AutofillPopupControllerUnitTest : public ChromeRenderViewHostTestHarness {
AutofillPopupControllerUnitTest()
: autofill_client_(new MockAutofillClient()),
autofill_popup_controller_(NULL) {}
~AutofillPopupControllerUnitTest() override {}
~AutofillPopupControllerUnitTest() override = default;
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
ContentAutofillDriverFactory::CreateForWebContentsAndDelegate(
web_contents(), autofill_client_.get(), "en-US",
AutofillManager::ENABLE_AUTOFILL_DOWNLOAD_MANAGER);
// Make sure RenderFrame is created.
NavigateAndCommit(GURL("about:blank"));
ContentAutofillDriverFactory* factory =
ContentAutofillDriverFactory::FromWebContents(web_contents());
ContentAutofillDriver* driver =
factory->DriverForFrame(web_contents()->GetMainFrame());
external_delegate_.reset(
new NiceMock<MockAutofillExternalDelegate>(
driver->autofill_manager(),
driver));
autofill_popup_view_.reset(new NiceMock<MockAutofillPopupView>());
external_delegate_ = CreateExternalDelegate();
autofill_popup_view_ = std::make_unique<NiceMock<MockAutofillPopupView>>();
autofill_popup_controller_ = new NiceMock<TestAutofillPopupController>(
external_delegate_->GetWeakPtr(), gfx::RectF());
autofill_popup_controller_->SetViewForTesting(autofill_popup_view());
......@@ -171,6 +229,21 @@ class AutofillPopupControllerUnitTest : public ChromeRenderViewHostTestHarness {
ChromeRenderViewHostTestHarness::TearDown();
}
virtual std::unique_ptr<NiceMock<MockAutofillExternalDelegate>>
CreateExternalDelegate() {
ContentAutofillDriverFactory::CreateForWebContentsAndDelegate(
web_contents(), autofill_client_.get(), "en-US",
AutofillManager::ENABLE_AUTOFILL_DOWNLOAD_MANAGER);
// Make sure RenderFrame is created.
NavigateAndCommit(GURL("about:blank"));
ContentAutofillDriverFactory* factory =
ContentAutofillDriverFactory::FromWebContents(web_contents());
ContentAutofillDriver* driver =
factory->DriverForFrame(web_contents()->GetMainFrame());
return std::make_unique<NiceMock<MockAutofillExternalDelegate>>(
driver->autofill_manager(), driver);
}
TestAutofillPopupController* popup_controller() {
return autofill_popup_controller_;
}
......@@ -190,6 +263,43 @@ class AutofillPopupControllerUnitTest : public ChromeRenderViewHostTestHarness {
NiceMock<TestAutofillPopupController>* autofill_popup_controller_;
};
#if !defined(OS_CHROMEOS)
class AutofillPopupControllerAccessibilityUnitTest
: public AutofillPopupControllerUnitTest {
public:
AutofillPopupControllerAccessibilityUnitTest() = default;
~AutofillPopupControllerAccessibilityUnitTest() override = default;
void SetUp() override {
AutofillPopupControllerUnitTest::SetUp();
content::BrowserAccessibilityState::GetInstance()
->AddAccessibilityModeFlags(ui::AXMode::kScreenReader);
}
void TearDown() override {
content::BrowserAccessibilityState::GetInstance()
->RemoveAccessibilityModeFlags(ui::AXMode::kScreenReader);
AutofillPopupControllerUnitTest::TearDown();
}
std::unique_ptr<NiceMock<MockAutofillExternalDelegate>>
CreateExternalDelegate() override {
autofill_driver_ = std::make_unique<NiceMock<MockAutofillDriver>>();
autofill_manager_ = std::make_unique<MockAutofillManager>(
autofill_driver_.get(), autofill_client_.get());
return std::make_unique<NiceMock<MockAutofillExternalDelegate>>(
autofill_manager_.get(), autofill_driver_.get());
}
protected:
std::unique_ptr<MockAutofillManager> autofill_manager_;
std::unique_ptr<NiceMock<MockAutofillDriver>> autofill_driver_;
private:
DISALLOW_COPY_AND_ASSIGN(AutofillPopupControllerAccessibilityUnitTest);
};
#endif
TEST_F(AutofillPopupControllerUnitTest, ChangeSelectedLine) {
// Set up the popup.
std::vector<Suggestion> suggestions;
......@@ -603,4 +713,111 @@ TEST_F(AutofillPopupControllerUnitTest, ElideText) {
}
#endif
#if !defined(OS_CHROMEOS)
TEST_F(AutofillPopupControllerAccessibilityUnitTest, FireControlsChangedEvent) {
StrictMock<MockAxTreeManager> mock_ax_tree_manager;
StrictMock<MockAxPlatformNodeDelegate> mock_ax_platform_node_delegate;
StrictMock<ui::AXPlatformNodeBase> mock_ax_platform_node;
const ui::AXTreeID& test_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeManagerMap::GetInstance().AddTreeManager(test_tree_id,
&mock_ax_tree_manager);
// Test for successfully firing controls changed event for popup show/hide.
{
EXPECT_CALL(*autofill_driver_, GetAxTreeId())
.Times(2)
.WillRepeatedly(testing::Return(test_tree_id));
EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId)
.Times(2)
.WillRepeatedly(testing::Return(base::Optional<int32_t>(123)));
EXPECT_CALL(mock_ax_tree_manager, GetDelegate)
.Times(2)
.WillRepeatedly(testing::Return(&mock_ax_platform_node_delegate));
EXPECT_CALL(mock_ax_platform_node_delegate, GetFromNodeID)
.Times(2)
.WillRepeatedly(testing::Return(&mock_ax_platform_node));
// Fire event for popup show and active popup ax unique id is set.
autofill_popup_controller_->FireControlsChangedEvent(true);
EXPECT_EQ(123, ui::GetActivePopupAxUniqueId());
// Fire event for popup hide and active popup ax unique id is cleared.
autofill_popup_controller_->FireControlsChangedEvent(false);
EXPECT_EQ(base::nullopt, ui::GetActivePopupAxUniqueId());
}
// Test for attempting to fire controls changed event when autofill driver
// returns an invalid ax tree id therefore no associated ax tree manager.
// No event is fired and global active popup ax unique id is not set.
{
EXPECT_CALL(*autofill_driver_, GetAxTreeId())
.WillOnce(testing::Return(ui::AXTreeIDUnknown()));
EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId).Times(0);
EXPECT_CALL(mock_ax_tree_manager, GetDelegate).Times(0);
EXPECT_CALL(mock_ax_platform_node_delegate, GetFromNodeID).Times(0);
// No controls changed event is fired and active popup ax unique id is not
// set.
autofill_popup_controller_->FireControlsChangedEvent(true);
EXPECT_EQ(base::nullopt, ui::GetActivePopupAxUniqueId());
}
// Test for attempting to fire controls changed event when ax tree manager
// fails to retrieve the ax platform node delegate associated with the popup.
// No event is fired and global active popup ax unique id is not set.
{
EXPECT_CALL(*autofill_driver_, GetAxTreeId())
.WillOnce(testing::Return(test_tree_id));
EXPECT_CALL(mock_ax_tree_manager, GetDelegate)
.WillOnce(testing::Return(nullptr));
EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId).Times(0);
EXPECT_CALL(mock_ax_platform_node_delegate, GetFromNodeID).Times(0);
// No controls changed event is fired and active popup ax unique id is not
// set.
autofill_popup_controller_->FireControlsChangedEvent(true);
EXPECT_EQ(base::nullopt, ui::GetActivePopupAxUniqueId());
}
// Test for attempting to fire controls changed event when failing to retrieve
// the ax platform node associated with the popup.
// No event is fired and global active popup ax unique id is not set.
{
EXPECT_CALL(*autofill_driver_, GetAxTreeId())
.WillOnce(testing::Return(test_tree_id));
EXPECT_CALL(mock_ax_tree_manager, GetDelegate)
.WillOnce(testing::Return(&mock_ax_platform_node_delegate));
EXPECT_CALL(mock_ax_platform_node_delegate, GetFromNodeID)
.WillOnce(testing::Return(nullptr));
EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId)
.WillOnce(testing::Return(base::Optional<int32_t>(123)));
// No controls changed event is fired and active popup ax unique id is not
// set.
autofill_popup_controller_->FireControlsChangedEvent(true);
EXPECT_EQ(base::nullopt, ui::GetActivePopupAxUniqueId());
}
// Test for attempting to fire controls changed event when failing to retrieve
// the autofill popup's ax unique id.
// No event is fired and global active popup ax unique id is not set.
{
EXPECT_CALL(*autofill_driver_, GetAxTreeId())
.WillOnce(testing::Return(test_tree_id));
EXPECT_CALL(mock_ax_tree_manager, GetDelegate)
.WillOnce(testing::Return(&mock_ax_platform_node_delegate));
EXPECT_CALL(mock_ax_platform_node_delegate, GetFromNodeID)
.WillOnce(testing::Return(&mock_ax_platform_node));
EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId)
.WillOnce(testing::Return(base::nullopt));
// No controls changed event is fired and active popup ax unique id is not
// set.
autofill_popup_controller_->FireControlsChangedEvent(true);
EXPECT_EQ(base::nullopt, ui::GetActivePopupAxUniqueId());
}
}
#endif
} // namespace autofill
......@@ -286,6 +286,10 @@ AutofillDriver* AutofillExternalDelegate::GetAutofillDriver() {
return driver_;
}
int32_t AutofillExternalDelegate::GetWebContentsPopupControllerAxId() const {
return query_field_.form_control_ax_id;
}
void AutofillExternalDelegate::RegisterDeletionCallback(
base::OnceClosure deletion_callback) {
deletion_callback_ = std::move(deletion_callback);
......@@ -295,10 +299,6 @@ void AutofillExternalDelegate::Reset() {
manager_->client()->HideAutofillPopup();
}
int32_t AutofillExternalDelegate::GetWebContentsPopupControllerAxId() const {
return query_field_.form_control_ax_id;
}
base::WeakPtr<AutofillExternalDelegate> AutofillExternalDelegate::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
......
......@@ -57,6 +57,10 @@ class AutofillExternalDelegate : public AutofillPopupDelegate {
// popup type after call to |onQuery|.
PopupType GetPopupType() const override;
AutofillDriver* GetAutofillDriver() override;
// Returns the ax node id associated with the current web contents' element
// who has a controller relation to the current autofill popup.
int32_t GetWebContentsPopupControllerAxId() const override;
void RegisterDeletionCallback(base::OnceClosure deletion_callback) override;
// Records and associates a query_id with web form data. Called
......@@ -96,10 +100,6 @@ class AutofillExternalDelegate : public AutofillPopupDelegate {
// values or settings.
void Reset();
// Returns the ax node id associated with the current web contents' element
// who has a controller relation to the current autofill popup.
int32_t GetWebContentsPopupControllerAxId() const;
const FormData& query_form() const { return query_form_; }
protected:
......
......@@ -56,6 +56,10 @@ class AutofillPopupDelegate {
// Returns the associated AutofillDriver.
virtual AutofillDriver* GetAutofillDriver() = 0;
// Returns the ax node id associated with the current web contents' element
// who has a controller relation to the current autofill popup.
virtual int32_t GetWebContentsPopupControllerAxId() const = 0;
// Sets |deletion_callback| to be called from the delegate's destructor.
// Useful for deleting objects which cannot be owned by the delegate but
// should not outlive it.
......
......@@ -260,6 +260,12 @@ autofill::AutofillDriver* PasswordAutofillManager::GetAutofillDriver() {
return password_manager_driver_->GetAutofillDriver();
}
int32_t PasswordAutofillManager::GetWebContentsPopupControllerAxId() const {
// TODO: Needs to be implemented once we step up accessibility features later.
NOTIMPLEMENTED_LOG_ONCE() << "See http://crbug.com/991253";
return 0;
}
void PasswordAutofillManager::RegisterDeletionCallback(
base::OnceClosure deletion_callback) {
deletion_callback_ = std::move(deletion_callback);
......
......@@ -55,6 +55,7 @@ class PasswordAutofillManager : public autofill::AutofillPopupDelegate {
void ClearPreviewedForm() override;
autofill::PopupType GetPopupType() const override;
autofill::AutofillDriver* GetAutofillDriver() override;
int32_t GetWebContentsPopupControllerAxId() const override;
void RegisterDeletionCallback(base::OnceClosure deletion_callback) override;
// Invoked when a password mapping is added.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment