Skip to content
Snippets Groups Projects
Commit b5e762c3 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot
Browse files

Merge remote-tracking branch 'dev/12-8-stable' into 12-8-stable

parents a9d7456c 785e16f1
No related branches found
No related tags found
No related merge requests found
Showing
with 571 additions and 17 deletions
Loading
Loading
@@ -6,9 +6,13 @@ describe Groups::GroupLinksController do
let(:shared_with_group) { create(:group, :private) }
let(:shared_group) { create(:group, :private) }
let(:user) { create(:user) }
let(:group_member) { create(:user) }
let!(:project) { create(:project, group: shared_group) }
 
before do
sign_in(user)
shared_with_group.add_developer(group_member)
end
 
describe '#create' do
Loading
Loading
@@ -40,13 +44,9 @@ describe Groups::GroupLinksController do
end
 
context 'when user has correct access to both groups' do
let(:group_member) { create(:user) }
before do
shared_with_group.add_developer(user)
shared_group.add_owner(user)
shared_with_group.add_developer(group_member)
end
 
context 'when default access level is requested' do
Loading
Loading
@@ -56,6 +56,10 @@ describe Groups::GroupLinksController do
context 'when owner access is requested' do
let(:shared_group_access) { Gitlab::Access::OWNER }
 
before do
shared_with_group.add_owner(group_member)
end
include_examples 'creates group group link'
 
it 'allows admin access for group member' do
Loading
Loading
@@ -64,6 +68,10 @@ describe Groups::GroupLinksController do
end
end
 
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:read_project, project) }.from(false).to(true)
end
context 'when shared with group id is not present' do
let(:shared_with_group_id) { nil }
 
Loading
Loading
@@ -149,6 +157,7 @@ describe Groups::GroupLinksController do
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
shared_with_group.refresh_members_authorized_projects
end
 
it 'updates existing link' do
Loading
Loading
@@ -162,6 +171,10 @@ describe Groups::GroupLinksController do
expect(link.group_access).to eq(Gitlab::Access::GUEST)
expect(link.expires_at).to eq(expiry_date)
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false)
end
end
 
context 'when user does not have admin access to the shared group' do
Loading
Loading
@@ -199,11 +212,16 @@ describe Groups::GroupLinksController do
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
shared_with_group.refresh_members_authorized_projects
end
 
it 'deletes existing link' do
expect { subject }.to change(GroupGroupLink, :count).by(-1)
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false)
end
end
 
context 'when user does not have admin access to the shared group' do
Loading
Loading
Loading
Loading
@@ -242,8 +242,10 @@ describe SnippetsController do
context 'when the snippet description contains a file' do
include FileMoverHelpers
 
let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" }
let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" }
let(:picture_secret) { SecureRandom.hex }
let(:text_secret) { SecureRandom.hex }
let(:picture_file) { "/-/system/user/#{user.id}/#{picture_secret}/picture.jpg" }
let(:text_file) { "/-/system/user/#{user.id}/#{text_secret}/text.txt" }
let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})"
Loading
Loading
@@ -266,8 +268,8 @@ describe SnippetsController do
snippet = subject
 
expected_description = "Description with picture: "\
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)"
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
 
expect(snippet.description).to eq(expected_description)
end
Loading
Loading
Loading
Loading
@@ -5,9 +5,9 @@ require "spec_helper"
describe "User creates a merge request", :js do
include ProjectForksHelper
 
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:title) { "Some feature" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
 
before do
project.add_maintainer(user)
Loading
Loading
@@ -38,6 +38,26 @@ describe "User creates a merge request", :js do
end
end
 
context "XSS branch name exists" do
before do
project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master")
end
it "doesn't execute the dodgy branch name" do
visit(project_new_merge_request_path(project))
find(".js-source-branch").click
click_link("<img/src='x'/onerror=alert('oops')>")
find(".js-target-branch").click
click_link("feature")
click_button("Compare branches")
expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
end
end
context "to a forked project" do
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
 
Loading
Loading
Loading
Loading
@@ -130,6 +130,28 @@ describe('ErrorDetails', () => {
expect(wrapper.findAll('button').length).toBe(3);
});
 
describe('unsafe chars for culprit field', () => {
const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
wrapper.setData({
error: {
culprit,
},
});
});
it('should not convert interpolated text to html entities', () => {
expect(findReportedText().findAll('script').length).toEqual(0);
expect(findReportedText().findAll('strong').length).toEqual(1);
});
it('should render text instead of converting to html entities', () => {
expect(findReportedText().text()).toContain(culprit);
});
});
describe('Badges', () => {
it('should show language and error level badges', () => {
wrapper.setData({
Loading
Loading
Loading
Loading
@@ -5,5 +5,9 @@ require 'spec_helper'
describe GitlabSchema.types['DiffRefs'] do
it { expect(described_class.graphql_name).to eq('DiffRefs') }
 
it { expect(described_class).to have_graphql_fields(:base_sha, :head_sha, :start_sha) }
it { is_expected.to have_graphql_fields(:head_sha, :base_sha, :start_sha).only }
it { expect(described_class.fields['headSha'].type).to be_non_null }
it { expect(described_class.fields['baseSha'].type).not_to be_non_null }
it { expect(described_class.fields['startSha'].type).to be_non_null }
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::AssetProxy do
context 'when asset proxy is disabled' do
before do
stub_asset_proxy_setting(enabled: false)
end
it 'returns the original URL' do
url = 'http://example.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
context 'when asset proxy is enabled' do
before do
stub_asset_proxy_setting(whitelist: %w(gitlab.com *.mydomain.com))
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret',
domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)
)
end
it 'returns a proxied URL' do
url = 'http://example.com/test.png'
proxied_url = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
expect(described_class.proxy_url(url)).to eq(proxied_url)
end
context 'whitelisted domain' do
it 'returns original URL for single domain whitelist' do
url = 'http://gitlab.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
it 'returns original URL for wildcard subdomain whitelist' do
url = 'http://test.mydomain.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizationsWithMinMaxUserId, :migration, schema: 20200204113224 do
let(:users_table) { table(:users) }
let(:min) { 1 }
let(:max) { 5 }
before do
min.upto(max) do |i|
users_table.create!(id: i, email: "user#{i}@example.com", projects_limit: 10)
end
end
describe '#perform' do
it 'initializes Users::RefreshAuthorizedProjectsService with correct users' do
min.upto(max) do |i|
user = User.find(i)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user, any_args).and_call_original)
end
described_class.new.perform(min, max)
end
it 'executes Users::RefreshAuthorizedProjectsService' do
expected_call_counts = max - min + 1
service = instance_double(Users::RefreshAuthorizedProjectsService)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).exactly(expected_call_counts).times.and_return(service))
expect(service).to receive(:execute).exactly(expected_call_counts).times
described_class.new.perform(min, max)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::DependencyLinker::BaseLinker do
let(:linker_class) do
Class.new(described_class) do
def link_dependencies
link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself)
end
end
end
let(:plain_content) do
<<~CONTENT
http://\\njavascript:alert(1)
https://gitlab.com/gitlab-org/gitlab
CONTENT
end
let(:highlighted_content) do
<<~CONTENT
<span><span>http://</span><span>\\n</span><span>javascript:alert(1)</span></span>
<span><span>https://gitlab.com/gitlab-org/gitlab</span></span>
CONTENT
end
let(:linker) { linker_class.new(plain_content, highlighted_content) }
describe '#link' do
subject { linker.link }
it 'only converts valid links' do
expect(subject).to eq(
<<~CONTENT
<span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
<span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span>
CONTENT
)
end
end
def link(text, url: text)
attrs = [
'rel="nofollow noreferrer noopener"',
'target="_blank"'
]
attrs.unshift(%{href="#{url}"}) if url
%{<a #{attrs.join(' ')}>#{text}</a>}
end
end
Loading
Loading
@@ -109,6 +109,20 @@ describe Gitlab::ProjectAuthorizations do
end
end
 
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'creates proper authorizations' do
group.add_reporter(user)
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[project_child.id]).to eq(Gitlab::Access::REPORTER)
end
end
context 'parent group user' do
let(:user) { parent_group_user }
 
Loading
Loading
Loading
Loading
@@ -30,6 +30,17 @@ describe Gitlab::UserAccess do
end
end
 
describe 'push to branch in an internal project' do
it 'will not infinitely loop when a project is internal' do
project.visibility_level = Gitlab::VisibilityLevel::INTERNAL
project.save!
expect(project).not_to receive(:branch_allows_collaboration?)
access.can_push_to_branch?('master')
end
end
describe 'push to empty project' do
let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) }
Loading
Loading
Loading
Loading
@@ -283,4 +283,18 @@ describe Gitlab::Utils do
expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
end
end
describe '.parse_url' do
it 'returns Addressable::URI object' do
expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI)
end
it 'returns nil when URI cannot be parsed' do
expect(described_class.parse_url('://gitlab.com')).to be nil
end
it 'returns nil with invalid parameter' do
expect(described_class.parse_url(1)).to be nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200214085940_clean_grafana_url.rb')
describe CleanGrafanaUrl, :migration do
let(:application_settings_table) { table(:application_settings) }
[
'javascript:alert(window.opener.document.location)',
' javascript:alert(window.opener.document.location)'
].each do |grafana_url|
it "sets grafana_url back to its default value when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq('/-/grafana')
end
end
['/-/grafana', '/some/relative/url', 'http://localhost:9000'].each do |grafana_url|
it "does not modify grafana_url when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq(grafana_url)
end
end
context 'when application_settings table has no rows' do
it 'does not fail' do
migrate!
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200204113224_schedule_recalculate_project_authorizations_second_run.rb')
describe ScheduleRecalculateProjectAuthorizationsSecondRun, :migration do
let(:users_table) { table(:users) }
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
1.upto(4) do |i|
users_table.create!(id: i, name: "user#{i}", email: "user#{i}@example.com", projects_limit: 1)
end
end
it 'schedules background migration' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION).to be_scheduled_migration(1, 2)
expect(described_class::MIGRATION).to be_scheduled_migration(3, 4)
end
end
end
end
Loading
Loading
@@ -19,6 +19,7 @@ describe ApplicationSetting do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
let(:ftp) { 'ftp://example.com' }
let(:javascript) { 'javascript:alert(window.opener.document.location)' }
 
it { is_expected.to allow_value(nil).for(:home_page_url) }
it { is_expected.to allow_value(http).for(:home_page_url) }
Loading
Loading
@@ -81,6 +82,53 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('abc').for(:minimum_password_length) }
it { is_expected.to allow_value(10).for(:minimum_password_length) }
 
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
end
it { is_expected.to allow_value(http).for(:grafana_url) }
it { is_expected.to allow_value(https).for(:grafana_url) }
it { is_expected.not_to allow_value(ftp).for(:grafana_url) }
it { is_expected.not_to allow_value(javascript).for(:grafana_url) }
it { is_expected.to allow_value('/-/grafana').for(:grafana_url) }
it { is_expected.to allow_value('http://localhost:9000').for(:grafana_url) }
context 'when local URLs are not allowed in system hooks' do
before do
stub_application_setting(allow_local_requests_from_system_hooks: false)
end
it { is_expected.not_to allow_value('http://localhost:9000').for(:grafana_url) }
end
context 'with invalid grafana URL' do
it 'adds an error' do
subject.grafana_url = ' ' + http
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'must be a valid relative or absolute URL. ' \
'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
context 'with blocked grafana URL' do
it 'adds an error' do
subject.grafana_url = javascript
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'is blocked: Only allowed schemes are http, https. Please check your ' \
'Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
end
context 'when snowplow is enabled' do
before do
setting.snowplow_enabled = true
Loading
Loading
Loading
Loading
@@ -91,6 +91,22 @@ describe Badge do
let(:method) { :image_url }
 
it_behaves_like 'rendered_links'
context 'when asset proxy is enabled' do
let(:placeholder_url) { 'http://www.example.com/image' }
before do
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret'
)
end
it 'returns a proxied URL' do
expect(badge.rendered_image_url).to start_with('https://assets.example.com')
end
end
end
end
end
Loading
Loading
@@ -563,6 +563,18 @@ describe Group do
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'returns correct access level' do
group.add_reporter(user)
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
end
 
context 'with user in the parent group' do
Loading
Loading
@@ -584,6 +596,33 @@ describe Group do
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.owner }
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'user without accepted access request' do
let!(:user) { create(:user) }
before do
create(:group_member, :developer, :access_request, user: user, group: group)
end
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
 
context 'when feature flag share_group_with_group is disabled' do
Loading
Loading
Loading
Loading
@@ -65,10 +65,10 @@ describe GroupMember do
end
 
describe '#update_two_factor_requirement' do
let(:user) { build :user }
let(:group_member) { build :group_member, user: user }
it 'is called after creation and deletion' do
user = build :user
group_member = build :group_member, user: user
expect(user).to receive(:update_two_factor_requirement)
 
group_member.save
Loading
Loading
@@ -79,6 +79,21 @@ describe GroupMember do
end
end
 
describe '#after_accept_invite' do
it 'calls #update_two_factor_requirement' do
email = 'foo@email.com'
user = build(:user, email: email)
group = create(:group, require_two_factor_authentication: true)
group_member = create(:group_member, group: group, invite_token: '1234', invite_email: email)
expect(user).to receive(:require_two_factor_authentication_from_group).and_call_original
group_member.accept_invite!(user)
expect(user.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'access levels' do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
Loading
Loading
Loading
Loading
@@ -4793,6 +4793,38 @@ describe Project do
end
end
 
context 'with cross internal project merge requests' do
let(:project) { create(:project, :repository, :internal) }
let(:forked_project) { fork_project(project, nil, repository: true) }
let(:user) { double(:user) }
it "does not endlessly loop for internal projects with MRs to each other", :sidekiq_inline do
allow(user).to receive(:can?).and_return(true, false, true)
allow(user).to receive(:id).and_return(1)
create(
:merge_request,
target_project: project,
target_branch: 'merge-test',
source_project: forked_project,
source_branch: 'merge-test',
allow_collaboration: true
)
create(
:merge_request,
target_project: forked_project,
target_branch: 'merge-test',
source_project: project,
source_branch: 'merge-test',
allow_collaboration: true
)
expect(user).to receive(:can?).at_most(5).times
project.branch_allows_collaboration?(user, "merge-test")
end
end
context 'with cross project merge requests' do
let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) }
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ describe Ci::PipelinePresenter do
include Gitlab::Routing
 
let(:user) { create(:user) }
let(:current_user) { user }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
 
Loading
Loading
@@ -15,7 +16,7 @@ describe Ci::PipelinePresenter do
 
before do
project.add_developer(user)
allow(presenter).to receive(:current_user) { user }
allow(presenter).to receive(:current_user) { current_user }
end
 
it 'inherits from Gitlab::View::Presenter::Delegated' do
Loading
Loading
@@ -224,10 +225,90 @@ describe Ci::PipelinePresenter do
describe '#all_related_merge_requests' do
it 'memoizes the returned relation' do
query_count = ActiveRecord::QueryRecorder.new do
2.times { presenter.send(:all_related_merge_requests).count }
3.times { presenter.send(:all_related_merge_requests).count }
end.count
 
expect(query_count).to eq(1)
expect(query_count).to eq(2)
end
context 'permissions' do
let!(:merge_request) do
create(:merge_request, project: project, source_project: project)
end
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
shared_examples 'private merge requests' do
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to be_empty }
end
context 'when logged in as a non_member' do
let(:current_user) { create(:user) }
it { is_expected.to be_empty }
end
context 'when logged in as a guest' do
let(:current_user) { create(:user) }
before do
project.add_guest(current_user)
end
it { is_expected.to be_empty }
end
context 'when logged in as a developer' do
it { is_expected.to contain_exactly(merge_request) }
end
context 'when logged in as a maintainer' do
let(:current_user) { create(:user) }
before do
project.add_maintainer(current_user)
end
it { is_expected.to contain_exactly(merge_request) }
end
end
context 'with a private project' do
it_behaves_like 'private merge requests'
end
context 'with a public project with private merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'private merge requests'
end
context 'with a public project with public merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::ENABLED)
end
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to contain_exactly(merge_request) }
end
end
end
end
 
Loading
Loading
Loading
Loading
@@ -116,6 +116,18 @@ describe API::Triggers do
end
end
end
context 'when is triggered by a pipeline hook' do
it 'does not create a new pipeline' do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"),
params: { ref: 'refs/heads/other-branch' },
headers: { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
end.not_to change(Ci::Pipeline, :count)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
 
describe 'GET /projects/:id/triggers' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment