Commit 276b5692 authored by John Abd-El-Malek's avatar John Abd-El-Malek Committed by Commit Bot

Implement http auth support in WebLayer.

Bug: 1025605
Change-Id: Ieb9958c948bc145a97c1146a5825944bcce79ffe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2209678
Commit-Queue: John Abd-El-Malek <jam@chromium.org>
Reviewed-by: default avatarMohamed Heikal <mheikal@chromium.org>
Reviewed-by: default avatarSam Maier <smaier@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarColin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#774982}
parent 7f37acf7
......@@ -32,12 +32,6 @@ template("standalone_system_webview_apk_tmpl") {
"//android_webview/nonembedded:system_webview_manifest"
deps = upstream_only_webview_deps
min_sdk_version = 21
# Material design is a large dependency that pulls in a lot of res/ files.
# At time of this comment, adding this dep even when unused adds 220kb.
assert_no_deps = [
"//third_party/android_deps:com_google_android_material_material_java",
]
}
}
......
......@@ -181,6 +181,56 @@ template("system_webview_apk_or_module_tmpl") {
resource_exclusion_regex = common_resource_exclusion_regex
resource_exclusion_exceptions = common_resource_exclusion_exceptions
_material_package = "*com_google_android_material_material*"
# These are used in WebLayer for HTTP Auth dialog.
resource_exclusion_exceptions += [
# TextInputLayout
"${_material_package}/design_text_*",
"${_material_package}/text_*",
]
# Copied from chrome_public_apk_tmpl.gni.
# Remove unneeded entries from material design values.xml files.
resource_values_filter_rules = [
"${_material_package}:[Bb]adge",
"${_material_package}:[Bb]ottomNavigation",
"${_material_package}:[Bb]ottomSheet",
"${_material_package}:[Bb]uttonToggleGroup",
"${_material_package}:[Cc]alendar",
"${_material_package}:[Cc]ardView",
"${_material_package}:\b[Cc]hip",
"${_material_package}:design_snackbar",
"${_material_package}:[Ff]loatingActionButton",
"${_material_package}:[Mm]aterialAlertDialog",
"${_material_package}:mtrl_alert",
"${_material_package}:mtrl_navigation",
"${_material_package}:mtrl_slider",
"${_material_package}:[Nn]avigationView",
"${_material_package}:picker",
"${_material_package}:[Ss]nackbar",
"${_material_package}:[Ss]lider",
"${_material_package}:[Tt]oolbarLayout",
]
_material_package = "com_google_android_material_material.*"
# Used only by alert dialog on tiny screens.
resource_exclusion_regex += "|${_material_package}values-small"
# Used only by date picker (which chrome doesn't use).
resource_exclusion_regex += "|${_material_package}-(w480dp-port|w360dp-port|h480dp-land|h360dp-land)"
# Material design layouts that cause views to be kept that we don't use.
# Instead of manually filtering, unused resource removal would be better:
# https://crbug.com/636448
resource_exclusion_regex += "|${_material_package}/layout"
resource_exclusion_regex += "|${_material_package}/color.*(choice|chip_|card_|calendar_|bottom_nav_|slider_)"
resource_exclusion_regex +=
"|${_material_package}/drawable.*design_snackbar"
resource_exclusion_regex += "|${_material_package}/xml.*badge_"
if (!_is_bundle_module) {
# Used as an additional apk in test scripts.
never_incremental = true
......
......@@ -174,6 +174,9 @@ template("chrome_public_common_apk_or_module_tmpl") {
resource_exclusion_regex += "|app_single_page_icon"
}
# Note most of these, with the exception of resource_exclusion_exceptions,
# are currently duplicated in system_webview_apk_tmpl.gni.
# Used only by alert dialog on tiny screens.
_material_package = "com_google_android_material_material.*"
resource_exclusion_regex += "|${_material_package}values-small"
......
......@@ -97,7 +97,7 @@ public class ChromeHttpAuthHandler extends EmptyTabObserver implements LoginProm
mTab.addObserver(this);
String messageBody = ChromeHttpAuthHandlerJni.get().getMessageBody(
mNativeChromeHttpAuthHandler, ChromeHttpAuthHandler.this);
mLoginPrompt = new LoginPrompt(activity, messageBody, this);
mLoginPrompt = new LoginPrompt(activity, messageBody, null, this);
// In case the autofill data arrives before the prompt is created.
if (mAutofillUsername != null && mAutofillPassword != null) {
mLoginPrompt.onAutofillDataAvailable(mAutofillUsername, mAutofillPassword);
......
......@@ -12,8 +12,10 @@ android_library("java") {
deps = [
":java_resources",
"//base:base_java",
"//components/browser_ui/widget/android:java",
"//components/strings:components_strings_grd",
"//third_party/android_deps:androidx_appcompat_appcompat_java",
"//third_party/android_deps:com_google_android_material_material_java",
"//ui/android:ui_java",
]
}
......
......@@ -20,7 +20,8 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_label"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
android:layout_height="wrap_content"
android:hint="@string/login_dialog_username_field" >
<org.chromium.components.browser_ui.widget.text.AlertDialogEditText
android:id="@+id/username"
android:layout_width="match_parent"
......@@ -34,7 +35,8 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
android:layout_height="wrap_content"
android:hint="@string/login_dialog_password_field" >
<org.chromium.components.browser_ui.widget.text.AlertDialogEditText
android:id="@+id/password"
android:layout_width="match_parent"
......
......@@ -6,15 +6,16 @@ package org.chromium.components.browser_ui.http_auth;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import org.chromium.components.browser_ui.widget.text.AlertDialogEditText;
import org.chromium.ui.UiUtils;
/**
......@@ -28,8 +29,8 @@ public class LoginPrompt {
private final Observer mObserver;
private AlertDialog mDialog;
private EditText mUsernameView;
private EditText mPasswordView;
private AlertDialogEditText mUsernameView;
private AlertDialogEditText mPasswordView;
/**
* This is a public interface that provides the result of the prompt.
......@@ -46,17 +47,35 @@ public class LoginPrompt {
public void proceed(String username, String password);
}
public LoginPrompt(Context context, String messageBody, Observer observer) {
/**
* Constructs an http auth prompt.
*
* @param context The Context to use.
* @param messageBody The text to show to the user.
* @param autofillUrl If not null, Android Autofill support is enabled for the form with the
* given url being set as the web domain for the View control.
* @param observer An interface to receive the result of the prompt.
*/
public LoginPrompt(Context context, String messageBody, String autofillUrl, Observer observer) {
mContext = context;
mMessageBody = messageBody;
mObserver = observer;
createDialog();
createDialog(autofillUrl);
}
private void createDialog() {
private void createDialog(String autofillUrl) {
View v = LayoutInflater.from(mContext).inflate(R.layout.http_auth_dialog, null);
mUsernameView = (EditText) v.findViewById(R.id.username);
mPasswordView = (EditText) v.findViewById(R.id.password);
mUsernameView = (AlertDialogEditText) v.findViewById(R.id.username);
mPasswordView = (AlertDialogEditText) v.findViewById(R.id.password);
if (autofillUrl != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// By default Android Autofill support is turned off for these controls because Chrome
// uses its own autofill provider (Chrome Sync). If an app is using Android Autofill
// then we need to enable Android Autofill for the controls.
mUsernameView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
mPasswordView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
mUsernameView.setUrl(autofillUrl);
mPasswordView.setUrl(autofillUrl);
}
mPasswordView.setOnEditorActionListener((v1, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
mDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
......
......@@ -4,11 +4,17 @@
package org.chromium.components.browser_ui.widget.text;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewStructure;
import android.widget.EditText;
import androidx.appcompat.widget.AppCompatEditText;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.annotations.VerifiesOnO;
/**
* Wrapper class needed due to b/122113958.
......@@ -18,14 +24,30 @@ import org.chromium.base.ApiCompatibilityUtils;
* calling {@link ApiCompatibilityUtils#setPasswordEditTextContentDescription(EditText)} after
* the change.
*/
@VerifiesOnO
public class AlertDialogEditText extends AppCompatEditText {
private String mUrl;
public AlertDialogEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setUrl(String url) {
mUrl = url;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
ApiCompatibilityUtils.setPasswordEditTextContentDescription(this);
}
@Override
@SuppressLint("NewApi")
public void onProvideAutofillStructure(ViewStructure structure, int flags) {
if (!TextUtils.isEmpty(mUrl)) {
structure.setWebDomain(mUrl);
}
super.onProvideAutofillStructure(structure, flags);
}
}
......@@ -165,6 +165,8 @@ source_set("weblayer_lib_base") {
"browser/file_select_helper.h",
"browser/host_content_settings_map_factory.cc",
"browser/host_content_settings_map_factory.h",
"browser/http_auth_handler_impl.cc",
"browser/http_auth_handler_impl.h",
"browser/i18n_util.cc",
"browser/i18n_util.h",
"browser/javascript_tab_modal_dialog_manager_delegate_android.cc",
......
......@@ -60,6 +60,7 @@
#include "weblayer/browser/browser_process.h"
#include "weblayer/browser/download_manager_delegate_impl.h"
#include "weblayer/browser/feature_list_creator.h"
#include "weblayer/browser/http_auth_handler_impl.h"
#include "weblayer/browser/i18n_util.h"
#include "weblayer/browser/navigation_controller_impl.h"
#include "weblayer/browser/profile_impl.h"
......@@ -684,6 +685,21 @@ ContentBrowserClientImpl::GetWideColorGamutHeuristic() {
// flinger.
return WideColorGamutHeuristic::kUseWindow;
}
std::unique_ptr<content::LoginDelegate>
ContentBrowserClientImpl::CreateLoginDelegate(
const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
const content::GlobalRequestID& request_id,
bool is_main_frame,
const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) {
return std::make_unique<HttpAuthHandlerImpl>(
auth_info, web_contents, first_auth_attempt,
std::move(auth_required_callback));
}
#endif // OS_ANDROID
content::SpeechRecognitionManagerDelegate*
......
......@@ -111,6 +111,15 @@ class ContentBrowserClientImpl : public content::ContentBrowserClient {
int child_process_id) override;
#if defined(OS_ANDROID)
WideColorGamutHeuristic GetWideColorGamutHeuristic() override;
std::unique_ptr<content::LoginDelegate> CreateLoginDelegate(
const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
const content::GlobalRequestID& request_id,
bool is_main_frame,
const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) override;
#endif // OS_ANDROID
void CreateFeatureListAndFieldTrials();
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "weblayer/browser/http_auth_handler_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/auth.h"
#include "weblayer/browser/tab_impl.h"
namespace weblayer {
HttpAuthHandlerImpl::HttpAuthHandlerImpl(
const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
bool first_auth_attempt,
LoginAuthRequiredCallback callback)
: WebContentsObserver(web_contents), callback_(std::move(callback)) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
url_ = auth_info.challenger.GetURL().Resolve(auth_info.path);
auto* tab = TabImpl::FromWebContents(web_contents);
tab->ShowHttpAuthPrompt(this);
}
HttpAuthHandlerImpl::~HttpAuthHandlerImpl() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* tab = TabImpl::FromWebContents(web_contents());
tab->CloseHttpAuthPrompt();
}
void HttpAuthHandlerImpl::Proceed(const base::string16& user,
const base::string16& password) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (callback_) {
std::move(callback_).Run(net::AuthCredentials(user, password));
}
}
void HttpAuthHandlerImpl::Cancel() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (callback_) {
std::move(callback_).Run(base::nullopt);
}
}
} // namespace weblayer
\ No newline at end of file
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef WEBLAYER_BROWSER_HTTP_AUTH_HANDLER_IMPL_H_
#define WEBLAYER_BROWSER_HTTP_AUTH_HANDLER_IMPL_H_
#include <memory>
#include <string>
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/login_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "url/gurl.h"
namespace weblayer {
// Implements support for http auth.
class HttpAuthHandlerImpl : public content::LoginDelegate,
public content::WebContentsObserver {
public:
HttpAuthHandlerImpl(const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
bool first_auth_attempt,
LoginAuthRequiredCallback callback);
~HttpAuthHandlerImpl() override;
void Proceed(const base::string16& user, const base::string16& password);
void Cancel();
GURL url() { return url_; }
private:
GURL url_;
LoginAuthRequiredCallback callback_;
};
} // namespace weblayer
#endif // WEBLAYER_BROWSER_HTTP_AUTH_HANDLER_IMPL_H_
\ No newline at end of file
......@@ -17,6 +17,7 @@ android_resources("weblayer_resources") {
custom_package = "org.chromium.weblayer_private"
deps = [
":weblayer_strings_grd",
"//components/browser_ui/http_auth/android:java_resources",
"//components/browser_ui/settings/android:java_resources",
"//components/browser_ui/site_settings/android:java_resources",
"//components/browser_ui/strings/android:browser_ui_strings_grd",
......@@ -119,6 +120,7 @@ android_library("java") {
"//base:base_java",
"//base:jni_java",
"//components/autofill/android:provider_java",
"//components/browser_ui/http_auth/android:java",
"//components/browser_ui/modaldialog/android:java",
"//components/browser_ui/notifications/android:java",
"//components/browser_ui/settings/android:java",
......
include_rules = [
"+components/browser_ui/http_auth",
"+components/browser_ui/util/android",
"+components/content_settings/android/java",
"+components/crash/android/java",
......
......@@ -27,6 +27,7 @@ import org.chromium.base.annotations.NativeMethods;
import org.chromium.components.autofill.AutofillActionModeCallback;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.browser_ui.http_auth.LoginPrompt;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
......@@ -69,7 +70,7 @@ import java.util.Map;
* Implementation of ITab.
*/
@JNINamespace("weblayer")
public final class TabImpl extends ITab.Stub {
public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
private static int sNextId = 1;
// Map from id to TabImpl.
private static final Map<Integer, TabImpl> sTabMap = new HashMap<Integer, TabImpl>();
......@@ -85,6 +86,7 @@ public final class TabImpl extends ITab.Stub {
private TabViewAndroidDelegate mViewAndroidDelegate;
// BrowserImpl this TabImpl is in. This is only null during creation.
private BrowserImpl mBrowser;
private LoginPrompt mLoginPrompt;
/**
* The AutofillProvider that integrates with system-level autofill. This is null until
* updateFromBrowser() is invoked.
......@@ -663,6 +665,27 @@ public final class TabImpl extends ITab.Stub {
getBrowser().destroyTab(this);
}
@CalledByNative
private void showHttpAuthPrompt(String host, String url) {
mLoginPrompt = new LoginPrompt(mBrowser.getContext(), host, url, this);
mLoginPrompt.show();
}
@CalledByNative
private void closeHttpAuthPrompt() {
mLoginPrompt = null;
}
@Override
public void cancel() {
TabImplJni.get().cancelHttpAuth(mNativeTab);
}
@Override
public void proceed(String username, String password) {
TabImplJni.get().setHttpAuth(mNativeTab, username, password);
}
public void destroy() {
// Ensure that this method isn't called twice.
assert mInterceptNavigationDelegate != null;
......@@ -824,5 +847,7 @@ public final class TabImpl extends ITab.Stub {
boolean setData(long nativeTabImpl, String[] data);
String[] getData(long nativeTabImpl);
boolean isRendererControllingBrowserControlsOffsets(long nativeTabImpl);
void setHttpAuth(long nativeTabImpl, String username, String password);
void cancelHttpAuth(long nativeTabImpl);
}
}
......@@ -76,6 +76,7 @@
#include "weblayer/browser/browser_controls_container_view.h"
#include "weblayer/browser/browser_controls_navigation_state_handler.h"
#include "weblayer/browser/controls_visibility_reason.h"
#include "weblayer/browser/http_auth_handler_impl.h"
#include "weblayer/browser/java/jni/TabImpl_jni.h"
#include "weblayer/browser/javascript_tab_modal_dialog_manager_delegate_android.h"
#include "weblayer/browser/weblayer_factory_impl_android.h"
......@@ -458,6 +459,28 @@ void TabImpl::ShowContextMenu(const content::ContextMenuParams& params) {
#endif
}
void TabImpl::ShowHttpAuthPrompt(HttpAuthHandlerImpl* auth_handler) {
CHECK(!auth_handler_);
auth_handler_ = auth_handler;
#if defined(OS_ANDROID)
JNIEnv* env = AttachCurrentThread();
GURL url = auth_handler_->url();
Java_TabImpl_showHttpAuthPrompt(
env, java_impl_, base::android::ConvertUTF8ToJavaString(env, url.host()),
base::android::ConvertUTF8ToJavaString(env, url.spec()));
#endif
}
void TabImpl::CloseHttpAuthPrompt() {
if (!auth_handler_)
return;
auth_handler_ = nullptr;
#if defined(OS_ANDROID)
JNIEnv* env = AttachCurrentThread();
Java_TabImpl_closeHttpAuthPrompt(env, java_impl_);
#endif
}
#if defined(OS_ANDROID)
// static
void TabImpl::DisableAutofillSystemIntegrationForTesting() {
......@@ -653,6 +676,20 @@ jboolean TabImpl::IsRendererControllingBrowserControlsOffsets(JNIEnv* env) {
->IsRendererControllingOffsets();
}
void TabImpl::SetHttpAuth(
JNIEnv* env,
const base::android::JavaParamRef<jstring>& username,
const base::android::JavaParamRef<jstring>& password) {
auth_handler_->Proceed(
base::android::ConvertJavaStringToUTF16(env, username),
base::android::ConvertJavaStringToUTF16(env, password));
CloseHttpAuthPrompt();
}
void TabImpl::CancelHttpAuth(JNIEnv* env) {
auth_handler_->Cancel();
CloseHttpAuthPrompt();
}
#endif // OS_ANDROID
content::WebContents* TabImpl::OpenURLFromTab(
......
......@@ -52,6 +52,7 @@ class FullscreenDelegate;
class NavigationControllerImpl;
class NewTabDelegate;
class ProfileImpl;
class HttpAuthHandlerImpl;
#if defined(OS_ANDROID)
class BrowserControlsContainerView;
......@@ -119,6 +120,9 @@ class TabImpl : public Tab,
void ShowContextMenu(const content::ContextMenuParams& params);
void ShowHttpAuthPrompt(HttpAuthHandlerImpl* auth_handler);
void CloseHttpAuthPrompt();
#if defined(OS_ANDROID)
base::android::ScopedJavaGlobalRef<jobject> GetJavaTab() {
return java_impl_;
......@@ -151,13 +155,11 @@ class TabImpl : public Tab,
void OnAutofillProviderChanged(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& autofill_provider);
void UpdateBrowserControlsState(JNIEnv* env,
jint raw_new_state,
jboolean animate);
base::android::ScopedJavaLocalRef<jstring> GetGuid(JNIEnv* env);
void CaptureScreenShot(
JNIEnv* env,
jfloat scale,
......@@ -167,6 +169,10 @@ class TabImpl : public Tab,
const base::android::JavaParamRef<jobjectArray>& data);
base::android::ScopedJavaLocalRef<jobjectArray> GetData(JNIEnv* env);
jboolean IsRendererControllingBrowserControlsOffsets(JNIEnv* env);
void SetHttpAuth(JNIEnv* env,
const base::android::JavaParamRef<jstring>& username,
const base::android::JavaParamRef<jstring>& password);
void CancelHttpAuth(JNIEnv* env);
#endif
ErrorPageDelegate* error_page_delegate() { return error_page_delegate_; }
......@@ -346,6 +352,8 @@ class TabImpl : public Tab,
base::string16 title_;
HttpAuthHandlerImpl* auth_handler_ = nullptr;
base::WeakPtrFactory<TabImpl> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(TabImpl);
......
......@@ -6,6 +6,7 @@ IDS_BEFORERELOAD_APP_MESSAGEBOX_TITLE
IDS_BEFORERELOAD_MESSAGEBOX_TITLE
IDS_BEFOREUNLOAD_APP_MESSAGEBOX_TITLE
IDS_BEFOREUNLOAD_MESSAGEBOX_TITLE
IDS_CANCEL
IDS_CAPTIVE_PORTAL_BUTTON_OPEN_LOGIN_PAGE
IDS_CAPTIVE_PORTAL_HEADING_WIFI
IDS_CAPTIVE_PORTAL_HEADING_WIRED
......@@ -95,6 +96,10 @@ IDS_JAVASCRIPT_MESSAGEBOX_TITLE
IDS_JAVASCRIPT_MESSAGEBOX_TITLE_IFRAME
IDS_JAVASCRIPT_MESSAGEBOX_TITLE_NONSTANDARD_URL
IDS_JAVASCRIPT_MESSAGEBOX_TITLE_NONSTANDARD_URL_IFRAME
IDS_LOGIN_DIALOG_USERNAME_FIELD
IDS_LOGIN_DIALOG_PASSWORD_FIELD
IDS_LOGIN_DIALOG_TITLE
IDS_LOGIN_DIALOG_OK_BUTTON_LABEL
IDS_MEDIA_CAPTURE_AUDIO_AND_VIDEO_INFOBAR_TEXT
IDS_MEDIA_CAPTURE_AUDIO_ONLY_INFOBAR_TEXT
IDS_MEDIA_CAPTURE_AUDIO_ONLY_PERMISSION_FRAGMENT
......
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