Commit 61b94b45 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu Committed by Douglas Barbosa Alexandre

Update Rails patches

Removes patches that are already in Rails 6:
Gitlab::ContentDisposition patch
ActiveRecord::QueryCache patch
ActiveJob::Arguments patch
Gitlab::ActionViewOutput patch

Updates Activerecord::Locking::Optimistic patch to follow changes in
https://github.com/rails/rails/blob/v6.0.1/activerecord/lib/active_record/locking/optimistic.rb

Fix missing timezone for datetime_with_timezone by adding this custom
type to ActiveRecord's time_zone_aware_attributes
parent a90fde60
...@@ -235,12 +235,6 @@ RSpec/FactoriesInMigrationSpecs: ...@@ -235,12 +235,6 @@ RSpec/FactoriesInMigrationSpecs:
- 'spec/lib/ee/gitlab/background_migration/**/*.rb' - 'spec/lib/ee/gitlab/background_migration/**/*.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb' - 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb'
Cop/IncludeActionViewContext:
Enabled: true
Exclude:
- 'spec/**/*'
- 'ee/spec/**/*'
Cop/IncludeSidekiqWorker: Cop/IncludeSidekiqWorker:
Enabled: true Enabled: true
Exclude: Exclude:
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module SendFileUpload module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment') def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
if attachment if attachment
response_disposition = ::Gitlab::ContentDisposition.format(disposition: disposition, filename: attachment) response_disposition = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: attachment)
# Response-Content-Type will not override an existing Content-Type in # Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for # Google Cloud Storage, so the metadata needs to be cleared on GCS for
...@@ -15,7 +15,7 @@ module SendFileUpload ...@@ -15,7 +15,7 @@ module SendFileUpload
# cross-origin JavaScript protection. # cross-origin JavaScript protection.
send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js' send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js'
send_params.merge!(filename: attachment, disposition: utf8_encoded_disposition(disposition, attachment)) send_params.merge!(filename: attachment, disposition: disposition)
end end
if file_upload.file_storage? if file_upload.file_storage?
...@@ -28,18 +28,6 @@ module SendFileUpload ...@@ -28,18 +28,6 @@ module SendFileUpload
end end
end end
# Since Rails 5 doesn't properly support support non-ASCII filenames,
# we have to add our own to ensure RFC 5987 compliance. However, Rails
# 5 automatically appends `filename#{filename}` here:
# https://github.com/rails/rails/blob/v5.0.7/actionpack/lib/action_controller/metal/data_streaming.rb#L137
# Rails 6 will have https://github.com/rails/rails/pull/33829, so we
# can get rid of this special case handling when we upgrade.
def utf8_encoded_disposition(disposition, filename)
content = ::Gitlab::ContentDisposition.new(disposition: disposition, filename: filename)
"#{disposition}; #{content.utf8_filename}"
end
def guess_content_type(filename) def guess_content_type(filename)
types = MIME::Types.type_for(filename) types = MIME::Types.type_for(filename)
......
...@@ -4,7 +4,7 @@ require 'nokogiri' ...@@ -4,7 +4,7 @@ require 'nokogiri'
module MarkupHelper module MarkupHelper
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
include ::Gitlab::ActionViewOutput::Context include ActionView::Context
def plain?(filename) def plain?(filename)
Gitlab::MarkupHelper.plain?(filename) Gitlab::MarkupHelper.plain?(filename)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module UserStatusTooltip module UserStatusTooltip
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
include ::Gitlab::ActionViewOutput::Context include ActionView::Context
include EmojiHelper include EmojiHelper
include UsersHelper include UsersHelper
......
...@@ -26,49 +26,26 @@ module MailScheduler ...@@ -26,49 +26,26 @@ module MailScheduler
end end
def self.perform_async(*args) def self.perform_async(*args)
super(*Arguments.serialize(args)) super(*ActiveJob::Arguments.serialize(args))
end end
private private
# If an argument is in the ActiveJob::Arguments::TYPE_WHITELIST list, # This is copied over from https://github.com/rails/rails/blob/v6.0.1/activejob/lib/active_job/arguments.rb#L50
# because it is declared as a private constant
PERMITTED_TYPES = [NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass].freeze
private_constant :PERMITTED_TYPES
# If an argument is in the PERMITTED_TYPES list,
# it means the argument cannot be deserialized. # it means the argument cannot be deserialized.
# Which means there's something wrong with our code. # Which means there's something wrong with our code.
def check_arguments!(args) def check_arguments!(args)
args.each do |arg| args.each do |arg|
if arg.class.in?(ActiveJob::Arguments::TYPE_WHITELIST) if arg.class.in?(PERMITTED_TYPES)
raise(ArgumentError, "Argument `#{arg}` cannot be deserialized because of its type") raise(ArgumentError, "Argument `#{arg}` cannot be deserialized because of its type")
end end
end end
end end
# Permit ActionController::Parameters for serializable Hash
#
# Port of
# https://github.com/rails/rails/commit/945fdd76925c9f615bf016717c4c8db2b2955357#diff-fc90ec41ef75be8b2259526fe1a8b663
module Arguments
include ActiveJob::Arguments
extend self
private
def serialize_argument(argument)
case argument
when -> (arg) { arg.respond_to?(:permitted?) }
serialize_hash(argument.to_h).tap do |result|
result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
end
else
super
end
end
end
# Make sure we remove this patch starting with Rails 6.0.
if Rails.version.start_with?('6.0')
raise <<~MSG
Please remove the patch `Arguments` module and use `ActiveJob::Arguments` again.
MSG
end
end end
end end
...@@ -24,10 +24,11 @@ module RegisterDateTimeWithTimeZone ...@@ -24,10 +24,11 @@ module RegisterDateTimeWithTimeZone
def initialize_type_map(mapping = type_map) def initialize_type_map(mapping = type_map)
super mapping super mapping
mapping.register_type 'timestamptz' do |_, _, sql_type| register_class_with_precision(
precision = extract_precision(sql_type) mapping,
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::DateTimeWithTimeZone.new(precision: precision) 'timestamptz',
end ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::DateTimeWithTimeZone
)
end end
end end
...@@ -46,3 +47,5 @@ end ...@@ -46,3 +47,5 @@ end
if (ActiveRecord::Base.connection.active? rescue false) if (ActiveRecord::Base.connection.active? rescue false)
ActiveRecord::Base.connection.send :reload_type_map ActiveRecord::Base.connection.send :reload_type_map
end end
ActiveRecord::Base.time_zone_aware_types += [:datetime_with_timezone]
# frozen_string_literal: true
ActiveRecord::ConnectionAdapters::ConnectionPool.prepend Gitlab::Patch::ActiveRecordQueryCache
...@@ -26,7 +26,7 @@ module ActiveRecord ...@@ -26,7 +26,7 @@ module ActiveRecord
locking_column => possible_previous_lock_value, locking_column => possible_previous_lock_value,
self.class.primary_key => id_in_database self.class.primary_key => id_in_database
).update_all( ).update_all(
attributes_with_values_for_update(attribute_names) attributes_with_values(attribute_names)
) )
if affected_rows != 1 if affected_rows != 1
......
...@@ -37,7 +37,7 @@ describe Projects::DesignsController do ...@@ -37,7 +37,7 @@ describe Projects::DesignsController do
subject subject
expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename*=UTF-8''#{filename}; filename=\"#{filename}\")) expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
end end
context 'when the design is not an LFS file' do context 'when the design is not an LFS file' do
......
...@@ -434,7 +434,7 @@ module API ...@@ -434,7 +434,7 @@ module API
def present_disk_file!(path, filename, content_type = 'application/octet-stream') def present_disk_file!(path, filename, content_type = 'application/octet-stream')
filename ||= File.basename(path) filename ||= File.basename(path)
header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'attachment', filename: filename) header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
header['Content-Transfer-Encoding'] = 'binary' header['Content-Transfer-Encoding'] = 'binary'
content_type content_type content_type content_type
...@@ -542,7 +542,7 @@ module API ...@@ -542,7 +542,7 @@ module API
def send_git_blob(repository, blob) def send_git_blob(repository, blob)
env['api.format'] = :txt env['api.format'] = :txt
content_type 'text/plain' content_type 'text/plain'
header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'inline', filename: blob.name) header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'inline', filename: blob.name)
# Let Workhorse examine the content and determine the better content disposition # Let Workhorse examine the content and determine the better content disposition
header[Gitlab::Workhorse::DETECT_HEADER] = "true" header[Gitlab::Workhorse::DETECT_HEADER] = "true"
......
# frozen_string_literal: true
# This file was simplified from https://raw.githubusercontent.com/rails/rails/195f39804a7a4a0034f25e8704220e03d95a752a/actionview/lib/action_view/context.rb.
#
# It is only needed by modules that need to call ActionView helper
# methods (e.g. those in
# https://github.com/rails/rails/tree/c4d3e202e10ae627b3b9c34498afb45450652421/actionview/lib/action_view/helpers)
# to generate tags outside of a Rails controller (e.g. API, Sidekiq,
# etc.).
#
# In Rails 5, ActionView::Context automatically includes CompiledTemplates.
# This means that any module that includes ActionView::Context is now a descendant
# of CompiledTemplates.
#
# When a partial is rendered for the first time, it runs
# Module#module_eval, which will evaluate a string source that defines a
# new method. For example:
#
# def _app_views_profiles_show_html_haml___1285955918103175884_70307801785400(local_assigns, output_buffer)
# "hello world"
# end
#
# When a new method is defined, the Ruby interpreter clears the method
# cache for all descendants, and all methods for those modules will have
# to be redefined. This can lead to a significant performance penalty.
#
# Rails 6 fixes this behavior by moving out the `include
# CompiledTemplates` into ActionView::Base so that including `ActionView::Context`
# doesn't quietly affect other modules in this way.
if Rails::VERSION::STRING.start_with?('6')
raise 'This module is no longer needed in Rails 6. Use ActionView::Context instead.'
end
module Gitlab
module ActionViewOutput
module Context
attr_accessor :output_buffer, :view_flow
end
end
end
# frozen_string_literal: true
# This ports ActionDispatch::Http::ContentDisposition (https://github.com/rails/rails/pull/33829,
# which will be available in Rails 6.
module Gitlab
class ContentDisposition # :nodoc:
# Make sure we remove this patch starting with Rails 6.0.
if Rails.version.start_with?('6.0')
raise <<~MSG
Please remove this file and use `ActionDispatch::Http::ContentDisposition` instead.
MSG
end
def self.format(disposition:, filename:)
new(disposition: disposition, filename: filename).to_s
end
attr_reader :disposition, :filename
def initialize(disposition:, filename:)
@disposition = disposition
@filename = filename
end
# rubocop:disable Style/VariableInterpolation
TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/.freeze
def ascii_filename
'filename="' + percent_escape(::I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
end
RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/.freeze
# rubocop:enable Style/VariableInterpolation
def utf8_filename
"filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
end
def to_s
if filename
"#{disposition}; #{ascii_filename}; #{utf8_filename}"
else
"#{disposition}"
end
end
private
def percent_escape(string, pattern)
string.gsub(pattern) do |char|
char.bytes.map { |byte| "%%%02X" % byte }.join
end
end
end
end
# frozen_string_literal: true
# Fixes a bug where the query cache isn't aware of the shared
# ActiveRecord connection used in tests
# https://github.com/rails/rails/issues/36587
# To be removed with https://gitlab.com/gitlab-org/gitlab-foss/issues/64413
module Gitlab
module Patch
module ActiveRecordQueryCache
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def enable_query_cache!
@query_cache_enabled[connection_cache_key(current_thread)] = true
connection.enable_query_cache! if active_connection?
end
def disable_query_cache!
@query_cache_enabled.delete connection_cache_key(current_thread)
connection.disable_query_cache! if active_connection?
end
def query_cache_enabled
@query_cache_enabled[connection_cache_key(current_thread)]
end
def active_connection?
@thread_cached_conns[connection_cache_key(current_thread)]
end
private
def current_thread
@lock_thread || Thread.current
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
# frozen_string_literal: true
module RuboCop
module Cop
# Cop that makes sure workers include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`.
class IncludeActionViewContext < RuboCop::Cop::Cop
MSG = 'Include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`, for Rails 5.'.freeze
def_node_matcher :includes_action_view_context?, <<~PATTERN
(send nil? :include (const (const nil? :ActionView) :Context))
PATTERN
def on_send(node)
return unless includes_action_view_context?(node)
add_offense(node.arguments.first, location: :expression)
end
def autocorrect(node)
lambda do |corrector|
corrector.replace(node.source_range, '::Gitlab::ActionViewOutput::Context')
end
end
end
end
end
...@@ -5,7 +5,6 @@ require_relative 'cop/gitlab/httparty' ...@@ -5,7 +5,6 @@ require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/finder_with_find_by' require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/gitlab/union' require_relative 'cop/gitlab/union'
require_relative 'cop/gitlab/rails_logger' require_relative 'cop/gitlab/rails_logger'
require_relative 'cop/include_action_view_context'
require_relative 'cop/include_sidekiq_worker' require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/safe_params' require_relative 'cop/safe_params'
require_relative 'cop/active_record_association_reload' require_relative 'cop/active_record_association_reload'
......
...@@ -59,11 +59,9 @@ describe SendFileUpload do ...@@ -59,11 +59,9 @@ describe SendFileUpload do
let(:params) { { disposition: 'inline', attachment: filename } } let(:params) { { disposition: 'inline', attachment: filename } }
it 'sends a file with inline disposition' do it 'sends a file with inline disposition' do
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
expected_params = { expected_params = {
filename: 'test.png', filename: 'test.png',
disposition: "inline; filename*=UTF-8''test.png" disposition: 'inline'
} }
expect(controller).to receive(:send_file).with(uploader.path, expected_params) expect(controller).to receive(:send_file).with(uploader.path, expected_params)
...@@ -76,34 +74,16 @@ describe SendFileUpload do ...@@ -76,34 +74,16 @@ describe SendFileUpload do
let(:params) { { attachment: filename } } let(:params) { { attachment: filename } }
it 'sends a file with content-type of text/plain' do it 'sends a file with content-type of text/plain' do
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
expected_params = { expected_params = {
content_type: 'text/plain', content_type: 'text/plain',
filename: 'test.js', filename: 'test.js',
disposition: "attachment; filename*=UTF-8''test.js" disposition: 'attachment'
} }
expect(controller).to receive(:send_file).with(uploader.path, expected_params) expect(controller).to receive(:send_file).with(uploader.path, expected_params)
subject subject
end end
context 'with non-ASCII encoded filename' do
let(:filename) { 'テスト.txt' }
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
it 'sends content-disposition for non-ASCII encoded filenames' do
expected_params = {
filename: filename,
disposition: "attachment; filename*=UTF-8''%E3%83%86%E3%82%B9%E3%83%88.txt"
}
expect(controller).to receive(:send_file).with(uploader.path, expected_params)
subject
end
end
context 'with a proxied file in object storage' do context 'with a proxied file in object storage' do
before do before do
stub_uploads_object_storage(uploader: uploader_class) stub_uploads_object_storage(uploader: uploader_class)
......
...@@ -138,14 +138,14 @@ describe Projects::ArtifactsController do ...@@ -138,14 +138,14 @@ describe Projects::ArtifactsController do
let(:filename) { job.artifacts_file.filename } let(:filename) { job.artifacts_file.filename }
it 'sends the artifacts file' do it 'sends the artifacts file' do
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
expect(controller).to receive(:send_file) expect(controller).to receive(:send_file)
.with( .with(
job.artifacts_file.file.path, job.artifacts_file.file.path,
hash_including(disposition: %Q(attachment; filename*=UTF-8''#{filename}))).and_call_original hash_including(disposition: 'attachment', filename: filename)).and_call_original
download_artifact download_artifact
expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end end
end end
...@@ -170,13 +170,13 @@ describe Projects::ArtifactsController do ...@@ -170,13 +170,13 @@ describe Projects::ArtifactsController do
end end
it 'sends the codequality report' do it 'sends the codequality report' do
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
expect(controller).to receive(:send_file) expect(controller).to receive(:send_file)
.with(job.job_artifacts_codequality.file.path, .with(job.job_artifacts_codequality.file.path,
hash_including(disposition: %Q(attachment; filename*=UTF-8''#{filename}))).and_call_original hash_including(disposition: 'attachment', filename: filename)).and_call_original
download_artifact(file_type: file_type) download_artifact(file_type: file_type)
expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end end
end end
......
...@@ -649,7 +649,7 @@ describe UploadsController do ...@@ -649,7 +649,7 @@ describe UploadsController do
get :show, params: { model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png' } get :show, params: { model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png' }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to end_with 'filename="dk.png"' expect(response.header['Content-Disposition']).to include('filename="dk.png"')
end end
end end
......
...@@ -9,7 +9,7 @@ describe "User downloads artifacts" do ...@@ -9,7 +9,7 @@ describe "User downloads artifacts" do
shared_examples "downloading" do shared_examples "downloading" do
it "downloads the zip" do it "downloads the zip" do
expect(page.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename*=UTF-8''#{job.artifacts_file.filename}; filename="#{job.artifacts_file.filename}"}) expect(page.response_headers['Content-Disposition']).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary") expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(page.response_headers['Content-Type']).to eq("application/zip") expect(page.response_headers['Content-Type']).to eq("application/zip")
expect(page.source.b).to eq(job.artifacts_file.file.read.b) expect(page.source.b).to eq(job.artifacts_file.file.read.b)
......
...@@ -346,7 +346,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -346,7 +346,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
artifact_request = requests.find { |req| req.url.match(%r{artifacts/download}) } artifact_request = requests.find { |req| req.url.match(%r{artifacts/download}) }
expect(artifact_request.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename*=UTF-8''#{job.artifacts_file.filename}; filename="#{job.artifacts_file.filename}"}) expect(artifact_request.response_headers['Content-Disposition']).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
expect(artifact_request.response_headers['Content-Transfer-Encoding']).to eq("binary") expect(artifact_request.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(artifact_request.response_headers['Content-Type']).to eq("image/gif") expect(artifact_request.response_headers['Content-Type']).to eq("image/gif")
expect(artifact_request.body).to eq(job.artifacts_file.file.read.b) expect(artifact_request.body).to eq(job.artifacts_file.file.read.b)
......
# frozen_string_literal: true
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/include_action_view_context'
describe RuboCop::Cop::IncludeActionViewContext do
include CopHelper
subject(:cop) { described_class.new }
context 'when `ActionView::Context` is included' do
let(:source) { 'include ActionView::Context' }
let(:correct_source) { 'include ::Gitlab::ActionViewOutput::Context' }
it 'registers an offense' do
inspect_source(source)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
expect(cop.highlights).to eq(['ActionView::Context'])
end
end
it 'autocorrects to the right version' do
autocorrected = autocorrect_source(source)
expect(autocorrected).to eq(correct_source)
end
end
context 'when `ActionView::Context` is not included' do
it 'registers no offense' do
inspect_source('include Context')
aggregate_failures do
expect(cop.offenses.size).to eq(0)
end
end
end
end
...@@ -41,13 +41,11 @@ RSpec.shared_examples 'a controller that can serve LFS files' do |options = {}| ...@@ -41,13 +41,11 @@ RSpec.shared_examples 'a controller that can serve LFS files' do |options = {}|
it 'serves the file' do it 'serves the file' do
lfs_uploader = LfsObjectUploader.new(lfs_object) lfs_uploader = LfsObjectUploader.new(lfs_object)
# Notice the filename= is omitted from the disposition; this is because
# Rails 5 will append this header in send_file
expect(controller).to receive(:send_file) expect(controller).to receive(:send_file)
.with( .with(
File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename), File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename),
filename: filename, filename: filename,
disposition: %Q(attachment; filename*=UTF-8''#{filename})) disposition: 'attachment')
subject subject
......
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