Commit 159ca7c4 authored by Bret Sepulveda's avatar Bret Sepulveda Committed by Commit Bot

Revert "Add CrostiniController class to manage foreground tasks"

This reverts commit b2e67312.

Reason for revert: Caused compile failure on Linux ChromiumOS MSan Builder: https://ci.chromium.org/p/chromium/builders/ci/Linux%20ChromiumOS%20MSan%20Builder/19874

Original change's description:
> Add CrostiniController class to manage foreground tasks
> 
> This is a refactor to move code out of file_manager.js and
> into crostini_controller.js in preparation to add more features
> for showing messages.
> 
> Updated UI tests to use FILES_NG_ENABLED and work with all files
> that import from chrome://resources such as files_message.html.
> 
> Bug: 1095889
> Change-Id: I9c9f34304e26dca8df4c0aa64a51c124650385b9
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2259873
> Reviewed-by: Luciano Pacheco <lucmult@chromium.org>
> Reviewed-by: Joel Hockey <joelhockey@chromium.org>
> Commit-Queue: Joel Hockey <joelhockey@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#781773}

TBR=joelhockey@chromium.org,lucmult@chromium.org

Change-Id: I91a7bd3837cf2ab2108ba672c36b3e5a5d5c743c
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1095889
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2263833Reviewed-by: default avatarBret Sepulveda <bsep@chromium.org>
Commit-Queue: Bret Sepulveda <bsep@chromium.org>
Cr-Commit-Position: refs/heads/master@{#781810}
parent 3e261594
...@@ -30,7 +30,6 @@ js_type_check("closure_compile_module") { ...@@ -30,7 +30,6 @@ js_type_check("closure_compile_module") {
":closure_compile_externs", ":closure_compile_externs",
":column_visibility_controller", ":column_visibility_controller",
":constants", ":constants",
":crostini_controller",
":dialog_action_controller", ":dialog_action_controller",
":dialog_type", ":dialog_type",
":directory_contents", ":directory_contents",
...@@ -234,10 +233,6 @@ js_library("column_visibility_controller") { ...@@ -234,10 +233,6 @@ js_library("column_visibility_controller") {
js_library("constants") { js_library("constants") {
} }
js_library("crostini_controller") {
deps = [ "ui:directory_tree" ]
}
js_library("dialog_action_controller") { js_library("dialog_action_controller") {
deps = [ deps = [
":dialog_type", ":dialog_type",
...@@ -334,7 +329,6 @@ js_library("file_manager") { ...@@ -334,7 +329,6 @@ js_library("file_manager") {
":android_app_list_model", ":android_app_list_model",
":app_state_controller", ":app_state_controller",
":column_visibility_controller", ":column_visibility_controller",
":crostini_controller",
":dialog_action_controller", ":dialog_action_controller",
":dialog_type", ":dialog_type",
":directory_model", ":directory_model",
......
// 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.
/**
* CrostiniController handles the foreground UI relating to crostini.
*/
class CrostiniController {
/**
* @param {!Crostini} crostini Crostini background object.
* @param {!DirectoryTree} directoryTree DirectoryTree.
*/
constructor(crostini, directoryTree) {
/** @private @const */
this.crostini_ = crostini;
/** @private @const */
this.directoryTree_ = directoryTree;
/** @private */
this.entrySharedWithCrostini_ = false;
/** @private */
this.entrySharedWithPluginVm_ = false;
}
/**
* Refresh the Linux files item at startup and when crostini enabled changes.
*/
async redraw() {
// Setup Linux files fake root.
this.directoryTree_.dataModel.linuxFilesItem =
this.crostini_.isEnabled(constants.DEFAULT_CROSTINI_VM) ?
new NavigationModelFakeItem(
str('LINUX_FILES_ROOT_LABEL'), NavigationModelItemType.CROSTINI,
new FakeEntry(
str('LINUX_FILES_ROOT_LABEL'),
VolumeManagerCommon.RootType.CROSTINI)) :
null;
// Redraw the tree to ensure 'Linux files' is added/removed.
this.directoryTree_.redraw(false);
}
/**
* Load the list of shared paths and show a toast if this is the first time
* that FilesApp is loaded since login.
*
* @param {boolean} maybeShowToast if true, show toast if this is the first
* time FilesApp is opened since login.
* @param {!FilesToast} filesToast
*/
async loadSharedPaths(maybeShowToast, filesToast) {
let showToast = maybeShowToast;
const getSharedPaths = async (vmName) => {
if (!this.crostini_.isEnabled(vmName)) {
return 0;
}
return new Promise(resolve => {
chrome.fileManagerPrivate.getCrostiniSharedPaths(
maybeShowToast, vmName, (entries, firstForSession) => {
showToast = showToast && firstForSession;
for (const entry of entries) {
this.crostini_.registerSharedPath(vmName, assert(entry));
}
resolve(entries.length);
});
});
};
const toast = (count, msgSingle, msgPlural, action, subPage, umaItem) => {
if (!showToast || count == 0) {
return;
}
filesToast.show(count == 1 ? str(msgSingle) : strf(msgPlural, count), {
text: str(action),
callback: () => {
chrome.fileManagerPrivate.openSettingsSubpage(subPage);
CommandHandler.recordMenuItemSelected(umaItem);
}
});
};
const [crostiniShareCount, pluginVmShareCount] = await Promise.all([
getSharedPaths(constants.DEFAULT_CROSTINI_VM),
getSharedPaths(constants.PLUGIN_VM)
]);
toast(
crostiniShareCount, 'FOLDER_SHARED_WITH_CROSTINI',
'FOLDER_SHARED_WITH_CROSTINI_PLURAL', 'MANAGE_TOAST_BUTTON_LABEL',
'crostini/sharedPaths',
CommandHandler.MenuCommandsForUMA.MANAGE_LINUX_SHARING_TOAST_STARTUP);
// TODO(crbug.com/949356): UX to provide guidance for what to do
// when we have shared paths with both Linux and Plugin VM.
toast(
pluginVmShareCount, 'FOLDER_SHARED_WITH_PLUGIN_VM',
'FOLDER_SHARED_WITH_PLUGIN_VM_PLURAL', 'MANAGE_TOAST_BUTTON_LABEL',
'app-management/pluginVm/sharedPaths',
CommandHandler.MenuCommandsForUMA
.MANAGE_PLUGIN_VM_SHARING_TOAST_STARTUP);
}
}
...@@ -29,9 +29,6 @@ class FileManager extends cr.EventTarget { ...@@ -29,9 +29,6 @@ class FileManager extends cr.EventTarget {
/** @private {?Crostini} */ /** @private {?Crostini} */
this.crostini_ = null; this.crostini_ = null;
/** @private {?CrostiniController} */
this.crostiniController_ = null;
/** /**
* ImportHistory. Non-null only once history observer is added in * ImportHistory. Non-null only once history observer is added in
* {@code addHistoryObserver}. * {@code addHistoryObserver}.
...@@ -1164,13 +1161,81 @@ class FileManager extends cr.EventTarget { ...@@ -1164,13 +1161,81 @@ class FileManager extends cr.EventTarget {
// multiple VMs. // multiple VMs.
chrome.fileManagerPrivate.onCrostiniChanged.addListener( chrome.fileManagerPrivate.onCrostiniChanged.addListener(
this.onCrostiniChanged_.bind(this)); this.onCrostiniChanged_.bind(this));
this.crostiniController_ = new CrostiniController( return this.setupCrostini_();
assert(this.crostini_), assert(this.directoryTree)); }
await this.crostiniController_.redraw();
// Never show toast in an open-file dialog. /**
const maybeShowToast = this.dialogType === DialogType.FULL_PAGE; * Sets up Crostini 'Linux files'.
return this.crostiniController_.loadSharedPaths( * @return {!Promise<void>}
maybeShowToast, this.ui_.toast); * @private
*/
async setupCrostini_() {
// Setup Linux files fake root.
this.directoryTree.dataModel.linuxFilesItem =
this.crostini_.isEnabled(constants.DEFAULT_CROSTINI_VM) ?
new NavigationModelFakeItem(
str('LINUX_FILES_ROOT_LABEL'), NavigationModelItemType.CROSTINI,
new FakeEntry(
str('LINUX_FILES_ROOT_LABEL'),
VolumeManagerCommon.RootType.CROSTINI)) :
null;
// Redraw the tree to ensure 'Linux files' is added/removed.
this.directoryTree.redraw(false);
// Load any existing shared paths.
// Only observe firstForSession when using full-page FilesApp.
// I.e., don't show toast in a dialog.
let showToast = false;
const getSharedPaths = async (vmName) => {
if (!this.crostini_.isEnabled(vmName)) {
return 0;
}
return new Promise(resolve => {
chrome.fileManagerPrivate.getCrostiniSharedPaths(
this.dialogType === DialogType.FULL_PAGE, vmName,
(entries, firstForSession) => {
showToast = showToast || firstForSession;
for (const entry of entries) {
this.crostini_.registerSharedPath(vmName, assert(entry));
}
resolve(entries.length);
});
});
};
const toast = (count, msgSingle, msgPlural, action, subPage, umaItem) => {
if (!showToast || count == 0) {
return;
}
this.ui_.toast.show(
count == 1 ? str(msgSingle) : strf(msgPlural, count), {
text: str(action),
callback: () => {
chrome.fileManagerPrivate.openSettingsSubpage(subPage);
CommandHandler.recordMenuItemSelected(umaItem);
}
});
};
const [crostiniShareCount, pluginVmShareCount] = await Promise.all([
getSharedPaths(constants.DEFAULT_CROSTINI_VM),
getSharedPaths(constants.PLUGIN_VM)
]);
toast(
crostiniShareCount, 'FOLDER_SHARED_WITH_CROSTINI',
'FOLDER_SHARED_WITH_CROSTINI_PLURAL', 'MANAGE_TOAST_BUTTON_LABEL',
'crostini/sharedPaths',
CommandHandler.MenuCommandsForUMA.MANAGE_LINUX_SHARING_TOAST_STARTUP);
// TODO(crbug.com/949356): UX to provide guidance for what to do
// when we have shared paths with both Linux and Plugin VM.
toast(
pluginVmShareCount, 'FOLDER_SHARED_WITH_PLUGIN_VM',
'FOLDER_SHARED_WITH_PLUGIN_VM_PLURAL', 'MANAGE_TOAST_BUTTON_LABEL',
'app-management/pluginVm/sharedPaths',
CommandHandler.MenuCommandsForUMA
.MANAGE_PLUGIN_VM_SHARING_TOAST_STARTUP);
} }
/** /**
...@@ -1190,11 +1255,11 @@ class FileManager extends cr.EventTarget { ...@@ -1190,11 +1255,11 @@ class FileManager extends cr.EventTarget {
switch (event.eventType) { switch (event.eventType) {
case chrome.fileManagerPrivate.CrostiniEventType.ENABLE: case chrome.fileManagerPrivate.CrostiniEventType.ENABLE:
this.crostini_.setEnabled(event.vmName, true); this.crostini_.setEnabled(event.vmName, true);
return this.crostiniController_.redraw(); return this.setupCrostini_();
case chrome.fileManagerPrivate.CrostiniEventType.DISABLE: case chrome.fileManagerPrivate.CrostiniEventType.DISABLE:
this.crostini_.setEnabled(event.vmName, false); this.crostini_.setEnabled(event.vmName, false);
return this.crostiniController_.redraw(); return this.setupCrostini_();
} }
} }
......
...@@ -107,7 +107,6 @@ ...@@ -107,7 +107,6 @@
// <include src="android_app_list_model.js"> // <include src="android_app_list_model.js">
// <include src="app_state_controller.js"> // <include src="app_state_controller.js">
// <include src="column_visibility_controller.js"> // <include src="column_visibility_controller.js">
// <include src="crostini_controller.js">
// <include src="dialog_action_controller.js"> // <include src="dialog_action_controller.js">
// <include src="dialog_type.js"> // <include src="dialog_type.js">
// <include src="directory_contents.js"> // <include src="directory_contents.js">
......
...@@ -32,7 +32,7 @@ crostiniMount.testMountCrostiniSuccess = async (done) => { ...@@ -32,7 +32,7 @@ crostiniMount.testMountCrostiniSuccess = async (done) => {
// Click on Linux files. // Click on Linux files.
assertTrue(test.fakeMouseClick(fakeRoot, 'click linux files')); assertTrue(test.fakeMouseClick(fakeRoot, 'click linux files'));
await test.waitForElement('files-spinner:not([hidden])'); await test.waitForElement('paper-progress:not([hidden])');
// Ensure mountCrostini is called. // Ensure mountCrostini is called.
await test.repeatUntil(() => { await test.repeatUntil(() => {
......
...@@ -22,10 +22,17 @@ chrome.fileManagerPrivate = { ...@@ -22,10 +22,17 @@ chrome.fileManagerPrivate = {
SHARE: 'share', SHARE: 'share',
UNSHARE: 'unshare', UNSHARE: 'unshare',
}, },
FormatFileSystemType: { Verb: {
VFAT: 'vfat', OPEN_WITH: 'open_with',
EXFAT: 'exfat', ADD_TO: 'add_to',
NTFS: 'ntfs', PACK_WITH: 'pack_with',
SHARE_WITH: 'share_with',
},
SearchType: {
ALL: 'ALL',
SHARED_WITH_ME: 'SHARED_WITH_ME',
EXCLUDE_DIRECTORIES: 'EXCLUDE_DIRECTORIES',
OFFLINE: 'OFFLINE',
}, },
DriveConnectionStateType: { DriveConnectionStateType: {
ONLINE: 'ONLINE', ONLINE: 'ONLINE',
...@@ -37,18 +44,6 @@ chrome.fileManagerPrivate = { ...@@ -37,18 +44,6 @@ chrome.fileManagerPrivate = {
NO_NETWORK: 'NO_NETWORK', NO_NETWORK: 'NO_NETWORK',
NO_SERVICE: 'NO_SERVICE', NO_SERVICE: 'NO_SERVICE',
}, },
SearchType: {
ALL: 'ALL',
SHARED_WITH_ME: 'SHARED_WITH_ME',
EXCLUDE_DIRECTORIES: 'EXCLUDE_DIRECTORIES',
OFFLINE: 'OFFLINE',
},
Verb: {
OPEN_WITH: 'open_with',
ADD_TO: 'add_to',
PACK_WITH: 'pack_with',
SHARE_WITH: 'share_with',
},
currentId_: 'test@example.com', currentId_: 'test@example.com',
displayedId_: 'test@example.com', displayedId_: 'test@example.com',
preferences_: { preferences_: {
......
...@@ -10,7 +10,7 @@ loadTimeData.data = $GRDP; ...@@ -10,7 +10,7 @@ loadTimeData.data = $GRDP;
// Extend with additional fields not found in grdp files. // Extend with additional fields not found in grdp files.
loadTimeData.overrideValues({ loadTimeData.overrideValues({
'CROSTINI_ENABLED': true, 'CROSTINI_ENABLED': true,
'FILES_NG_ENABLED': true, 'FILES_NG_ENABLED': false,
'FILES_TRANSFER_DETAILS_ENABLED': false, 'FILES_TRANSFER_DETAILS_ENABLED': false,
'FEEDBACK_PANEL_ENABLED': false, 'FEEDBACK_PANEL_ENABLED': false,
'GOOGLE_DRIVE_REDEEM_URL': 'http://www.google.com/intl/en/chrome/devices' + 'GOOGLE_DRIVE_REDEEM_URL': 'http://www.google.com/intl/en/chrome/devices' +
......
...@@ -34,11 +34,13 @@ output = args.output or os.path.abspath( ...@@ -34,11 +34,13 @@ output = args.output or os.path.abspath(
# SRC : Absolute path to //src/. # SRC : Absolute path to //src/.
# GEN : Absolute path to $target_gen_dir. # GEN : Absolute path to $target_gen_dir.
# ROOT : Relative path from GEN to //src/ui/file_manager/file_manager. # ROOT : Relative path from GEN to //src/ui/file_manager/file_manager.
# R_GEN : Directory where chrome://resources/ is copied to. # CC_SRC: Source directory for components-chromium.
# CC_GEN: Directory where components-chromium is copied to.
SRC = os.path.abspath(os.path.join(sys.path[0], '../../../../..')) + '/' SRC = os.path.abspath(os.path.join(sys.path[0], '../../../../..')) + '/'
GEN = os.path.dirname(os.path.abspath(args.output)) + '/' GEN = os.path.dirname(os.path.abspath(args.output)) + '/'
ROOT = os.path.relpath(SRC, GEN) + '/ui/file_manager/file_manager/' ROOT = os.path.relpath(SRC, GEN) + '/ui/file_manager/file_manager/'
R_GEN = 'test/gen/resources/' CC_SRC = 'third_party/polymer/v1_0/components-chromium/'
CC_GEN = 'test/gen/cc/'
scripts = [] scripts = []
GENERATED = 'Generated at %s by: %s' % (time.ctime(), sys.path[0]) GENERATED = 'Generated at %s by: %s' % (time.ctime(), sys.path[0])
GENERATED_HTML = '<!-- %s -->\n\n' % GENERATED GENERATED_HTML = '<!-- %s -->\n\n' % GENERATED
...@@ -120,33 +122,25 @@ def i18n(template): ...@@ -120,33 +122,25 @@ def i18n(template):
repl = lambda x: strings.get(x.group(1), x.group()) repl = lambda x: strings.get(x.group(1), x.group())
return re.sub(r'\$i18n(?:Raw)?\{(.*?)\}', repl, template) return re.sub(r'\$i18n(?:Raw)?\{(.*?)\}', repl, template)
# Copy tree from src_dir to dst_dir with substitutions.
def copytree(src_dir, dst_dir):
for root, _, files in os.walk(SRC + src_dir):
for f in files:
srcf = os.path.join(root[len(SRC):], f)
dstf = dst_dir + srcf[len(src_dir):]
write(dstf, i18n(read(srcf).replace('chrome://resources/', GEN + R_GEN)))
# Copy any files required in chrome://resources/... into test/gen/resources.
copytree('ui/webui/resources/', R_GEN)
copytree('third_party/polymer/v1_0/components-chromium/',
R_GEN + 'polymer/v1_0/')
shutil.rmtree(GEN + R_GEN + 'polymer/v1_0/polymer')
os.rename(GEN + R_GEN + 'polymer/v1_0/polymer2',
GEN + R_GEN + 'polymer/v1_0/polymer')
shutil.copy(GEN + '../../webui/resources/css/cros_colors.generated.css',
GEN + R_GEN + 'css/')
# Substitute $i18n{}. # Substitute $i18n{}.
# Update relative paths, and paths to chrome://resources/. # Update relative paths.
# Fix link to action_link.css and text_defaults.css.
# Fix stylesheet from extension.
main_html = (i18n(read('ui/file_manager/file_manager/main.html')) main_html = (i18n(read('ui/file_manager/file_manager/main.html'))
.replace('chrome://resources/polymer/v1_0/', '../../../' + CC_SRC)
.replace('chrome://resources/css/action_link.css',
'../../webui/resources/css/action_link.css')
.replace('href="', 'href="' + ROOT) .replace('href="', 'href="' + ROOT)
.replace('src="', 'src="' + ROOT) .replace('src="', 'src="' + ROOT)
.replace(ROOT + 'chrome://resources/', R_GEN) .replace(ROOT + 'chrome://resources/html/', CC_GEN + 'polymer/')
.replace(ROOT + 'chrome://resources/css/text_defaults.css',
'test/gen/css/text_defaults.css')
.split('\n')) .split('\n'))
# Fix text_defaults.css. Copy and replace placeholders.
text_defaults = i18n(read('ui/webui/resources/css/text_defaults.css'))
write('test/gen/css/text_defaults.css', text_defaults)
# Add scripts required for testing, and the test files (test/*.js). # Add scripts required for testing, and the test files (test/*.js).
src = [ src = [
'test/js/chrome_api_test_impl.js', 'test/js/chrome_api_test_impl.js',
...@@ -199,6 +193,15 @@ main_html = replaceline(main_html, 'foreground/js/main_scripts.js', [ ...@@ -199,6 +193,15 @@ main_html = replaceline(main_html, 'foreground/js/main_scripts.js', [
def elements_path(elements_filename): def elements_path(elements_filename):
return '../../../../%sforeground/elements/%s' % (ROOT, elements_filename) return '../../../../%sforeground/elements/%s' % (ROOT, elements_filename)
# Copy all files from //third_party/polymer/v1_0/components-chromium/ into
# test/gen/cc. Rename polymer2 to polymer.
gen_cc = GEN + CC_GEN
if os.path.exists(gen_cc):
shutil.rmtree(gen_cc)
shutil.copytree(SRC + CC_SRC, gen_cc)
shutil.rmtree(gen_cc + 'polymer')
os.rename(gen_cc + 'polymer2', gen_cc + 'polymer')
# Generate strings.js # Generate strings.js
# Copy all html files in foreground/elements and fix references to polymer # Copy all html files in foreground/elements and fix references to polymer
# Load QuickView in iframe rather than webview. # Load QuickView in iframe rather than webview.
...@@ -206,13 +209,13 @@ for filename, substitutions in ( ...@@ -206,13 +209,13 @@ for filename, substitutions in (
('test/js/strings.js', ( ('test/js/strings.js', (
('$GRDP', json.dumps(strings, sort_keys=True, indent=2)), ('$GRDP', json.dumps(strings, sort_keys=True, indent=2)),
)), )),
('foreground/elements/elements_bundle.html', ()), ('foreground/elements/elements_bundle.html', (
('="files_xf', '="' + elements_path('files_xf')),
)),
('foreground/js/elements_importer.js', ( ('foreground/js/elements_importer.js', (
("= 'foreground", "= 'test/gen/foreground"), ("= 'foreground", "= 'test/gen/foreground"),
)), )),
('foreground/elements/files_format_dialog.html', ()),
('foreground/elements/files_icon_button.html', ()), ('foreground/elements/files_icon_button.html', ()),
('foreground/elements/files_message.html', ()),
('foreground/elements/files_metadata_box.html', ()), ('foreground/elements/files_metadata_box.html', ()),
('foreground/elements/files_metadata_entry.html', ()), ('foreground/elements/files_metadata_entry.html', ()),
('foreground/elements/files_quick_view.html', ( ('foreground/elements/files_quick_view.html', (
...@@ -232,19 +235,23 @@ for filename, substitutions in ( ...@@ -232,19 +235,23 @@ for filename, substitutions in (
('this.webview_.contentWindow.content.type = this.type;' ('this.webview_.contentWindow.content.type = this.type;'
'this.webview_.contentWindow.content.src = this.src;')), 'this.webview_.contentWindow.content.src = this.src;')),
)), )),
('foreground/elements/files_spinner.html', ()),
('foreground/elements/files_toast.html', ()), ('foreground/elements/files_toast.html', ()),
('foreground/elements/files_toggle_ripple.html', ()), ('foreground/elements/files_toggle_ripple.html', ()),
('foreground/elements/files_tooltip.html', ()), ('foreground/elements/files_tooltip.html', ()),
('foreground/elements/files_xf_elements.html', (
('src="xf_',
'src="%sui/file_manager/file_manager/foreground/elements/xf_' % SRC),
)),
('foreground/elements/icons.html', ()), ('foreground/elements/icons.html', ()),
): ):
buf = i18n(read('ui/file_manager/file_manager/' + filename)) buf = i18n(read('ui/file_manager/file_manager/' + filename))
buf = buf.replace('chrome://resources/', '../../resources/') buf = buf.replace('chrome://resources/html/', '../../cc/polymer/')
buf = buf.replace('chrome://resources/polymer/v1_0/', '../../cc/')
buf = buf.replace('<link rel="import" href="chrome://resources/cr_elements/'
'cr_input/cr_input.html">', '')
buf = buf.replace('<link rel="import" href="chrome://resources/cr_elements/'
'cr_button/cr_button.html">', '')
buf = buf.replace('src="files_', 'src="' + elements_path('files_')) buf = buf.replace('src="files_', 'src="' + elements_path('files_'))
# The files_format_dialog and files_message import various files that are
# not available in a ui test, just ignore them completely.
buf = buf.replace('<link rel="import" href="files_format_dialog.html">', '')
buf = buf.replace('<link rel="import" href="files_message.html">', '')
for old, new in substitutions: for old, new in substitutions:
buf = buf.replace(old, new) buf = buf.replace(old, new)
write('test/gen/' + filename, buf) write('test/gen/' + filename, buf)
......
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