Commit 8ec1f73d authored by mo khan's avatar mo khan Committed by Stan Hu

Parse path from license_scanning report

* Update usages of 'add_dependency'
* Add changelog entry
* Calculate blob_path from partial path
parent be2e94f3
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
class LicenseEntity < Grape::Entity class LicenseEntity < Grape::Entity
class ComponentEntity < Grape::Entity class ComponentEntity < Grape::Entity
expose :name expose :name
expose :path, as: :blob_path expose :blob_path do |model, options|
model.blob_path_for(options[:project])
end
end end
expose :id expose :id
......
---
title: Generate a link to an artifact using the path from the license scanning report
merge_request: 43455
author:
type: fixed
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
json.fetch(:dependencies, []).each do |dependency| json.fetch(:dependencies, []).each do |dependency|
each_license_for(dependency) do |license_hash| each_license_for(dependency) do |license_hash|
license = report.add_license(id: nil, name: license_hash[:name], url: license_hash[:url]) license = report.add_license(id: nil, name: license_hash[:name], url: license_hash[:url])
license.add_dependency(dependency[:dependency][:name]) license.add_dependency(name: dependency[:dependency][:name])
end end
end end
end end
......
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
def add_dependencies(report_hash) def add_dependencies(report_hash)
report_hash[:dependencies].each do |dependency_hash| report_hash[:dependencies].each do |dependency_hash|
dependency_hash[:licenses].map do |license_id| dependency_hash[:licenses].map do |license_id|
license_for(license_id).add_dependency(dependency_hash[:name]) license_for(license_id).add_dependency(dependency_hash)
end end
end end
end end
......
...@@ -6,11 +6,22 @@ module Gitlab ...@@ -6,11 +6,22 @@ module Gitlab
module LicenseScanning module LicenseScanning
class Dependency class Dependency
attr_accessor :path attr_accessor :path
attr_reader :name attr_reader :name, :package_manager, :version
def initialize(name, path: nil) def initialize(attributes = {})
@name = name @name = attributes.fetch(:name)
@path = path @package_manager = attributes[:package_manager]
@path = attributes[:path]
@version = attributes[:version]
end
def blob_path_for(project, sha: project&.default_branch_or_master)
return if path.blank?
return path if sha.blank?
::Gitlab::Routing
.url_helpers
.project_blob_path(project, File.join(sha, path))
end end
def hash def hash
......
...@@ -54,8 +54,8 @@ module Gitlab ...@@ -54,8 +54,8 @@ module Gitlab
canonical_id.hash canonical_id.hash
end end
def add_dependency(name) def add_dependency(attributes = {})
@dependencies.add(::Gitlab::Ci::Reports::LicenseScanning::Dependency.new(name)) @dependencies.add(::Gitlab::Ci::Reports::LicenseScanning::Dependency.new(attributes))
end end
def dependencies def dependencies
......
...@@ -10,21 +10,21 @@ FactoryBot.define do ...@@ -10,21 +10,21 @@ FactoryBot.define do
end end
trait :report_1 do trait :report_1 do
after(:build) do |report, evaluator| after(:build) do |report, evaluator|
report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency('Library1') report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency(name: 'Library1')
report.add_license(id: 'WTFPL', name: 'WTFPL', url: 'https://opensource.org/licenses/wtfpl').add_dependency('Library2') report.add_license(id: 'WTFPL', name: 'WTFPL', url: 'https://opensource.org/licenses/wtfpl').add_dependency(name: 'Library2')
end end
end end
trait :report_2 do trait :report_2 do
after(:build) do |report, evaluator| after(:build) do |report, evaluator|
report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency('Library1') report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency(name: 'Library1')
report.add_license(id: 'Apache-2.0', name: 'Apache 2.0', url: 'https://opensource.org/licenses/apache').add_dependency('Library3') report.add_license(id: 'Apache-2.0', name: 'Apache 2.0', url: 'https://opensource.org/licenses/apache').add_dependency(name: 'Library3')
end end
end end
trait :mit do trait :mit do
after(:build) do |report, evaluator| after(:build) do |report, evaluator|
report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency('rails') report.add_license(id: 'MIT', name: 'MIT', url: 'https://opensource.org/licenses/mit').add_dependency(name: 'rails')
end end
end end
end end
......
...@@ -122,6 +122,18 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do ...@@ -122,6 +122,18 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do
expect(report.licenses[0].count).to be(2) expect(report.licenses[0].count).to be(2)
expect(report.licenses[0].dependencies.count).to be(2) expect(report.licenses[0].dependencies.count).to be(2)
expect(report.licenses[0].dependencies.map(&:name)).to contain_exactly('b', 'c') expect(report.licenses[0].dependencies.map(&:name)).to contain_exactly('b', 'c')
dependency_b = report.licenses[0].dependencies.find { |x| x.name == 'b' }
expect(dependency_b.name).to eql('b')
expect(dependency_b.version).to eql('0.1.0')
expect(dependency_b.path).to eql('yarn.lock')
expect(dependency_b.package_manager).to eql('yarn')
dependency_c = report.licenses[0].dependencies.find { |x| x.name == 'c' }
expect(dependency_c.name).to eql('c')
expect(dependency_c.version).to eql('1.1.0')
expect(dependency_c.path).to eql('Gemfile.lock')
expect(dependency_c.package_manager).to eql('bundler')
end end
it 'parses the MIT license' do it 'parses the MIT license' do
...@@ -131,6 +143,18 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do ...@@ -131,6 +143,18 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do
expect(report.licenses[1].count).to be(2) expect(report.licenses[1].count).to be(2)
expect(report.licenses[1].dependencies.count).to be(2) expect(report.licenses[1].dependencies.count).to be(2)
expect(report.licenses[1].dependencies.map(&:name)).to contain_exactly('a', 'c') expect(report.licenses[1].dependencies.map(&:name)).to contain_exactly('a', 'c')
dependency_a = report.licenses[1].dependencies.find { |x| x.name == 'a' }
expect(dependency_a.name).to eql('a')
expect(dependency_a.version).to eql('1.0.0')
expect(dependency_a.path).to eql('Gemfile.lock')
expect(dependency_a.package_manager).to eql('bundler')
dependency_c = report.licenses[1].dependencies.find { |x| x.name == 'c' }
expect(dependency_c.name).to eql('c')
expect(dependency_c.version).to eql('1.1.0')
expect(dependency_c.path).to eql('Gemfile.lock')
expect(dependency_c.package_manager).to eql('bundler')
end end
it 'parses an unknown license' do it 'parses an unknown license' do
...@@ -140,6 +164,11 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do ...@@ -140,6 +164,11 @@ RSpec.describe Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning do
expect(report.licenses[2].count).to be(1) expect(report.licenses[2].count).to be(1)
expect(report.licenses[2].dependencies.count).to be(1) expect(report.licenses[2].dependencies.count).to be(1)
expect(report.licenses[2].dependencies.map(&:name)).to contain_exactly('d') expect(report.licenses[2].dependencies.map(&:name)).to contain_exactly('d')
expect(report.licenses[2].dependencies[0].name).to eql('d')
expect(report.licenses[2].dependencies[0].version).to eql('1.1.1')
expect(report.licenses[2].dependencies[0].package_manager).to eql('bundler')
expect(report.licenses[2].dependencies[0].path).to eql('Gemfile.lock')
end end
end end
......
...@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do ...@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do
let(:license) { build(:ci_reports_license_scanning_report, :mit).licenses.first } let(:license) { build(:ci_reports_license_scanning_report, :mit).licenses.first }
before do before do
license.add_dependency(name_of_dependency_with_license) license.add_dependency(name: name_of_dependency_with_license)
report.add_dependency(dependency) report.add_dependency(dependency)
report.apply_license(license) report.apply_license(license)
end end
......
...@@ -7,12 +7,47 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Dependency do ...@@ -7,12 +7,47 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Dependency do
let(:set) { Set.new } let(:set) { Set.new }
it 'cannot add the same dependency to a set twice' do it 'cannot add the same dependency to a set twice' do
set.add(described_class.new('bundler')) set.add(described_class.new(name: 'bundler'))
set.add(described_class.new('bundler')) set.add(described_class.new(name: 'bundler'))
expect(set.count).to eq(1) expect(set.count).to eq(1)
end end
it { expect(described_class.new('bundler')).to eql(described_class.new('bundler')) } it { expect(described_class.new(name: 'bundler')).to eql(described_class.new(name: 'bundler')) }
end
describe "#blob_path_for" do
let(:dependency) { described_class.new(name: 'rails', path: lockfile) }
let(:lockfile) { 'Gemfile.lock' }
context "when a project, sha and path are provided" do
subject { dependency.blob_path_for(build.project, sha: build.sha)}
let(:build) { build_stubbed(:ee_ci_build, :success, :license_scan_v2) }
specify { expect(subject).to eql("/#{build.project.namespace.path}/#{build.project.name}/-/blob/#{build.sha}/#{lockfile}") }
end
context "when a path is not available" do
subject { dependency.blob_path_for(build_stubbed(:project))}
let(:lockfile) { nil }
specify { expect(subject).to be_nil }
end
context "when a project is not provided" do
subject { dependency.blob_path_for(nil)}
specify { expect(subject).to eql(lockfile) }
end
context "when a sha is not provided" do
subject { dependency.blob_path_for(project) }
let(:project) { build_stubbed(:project) }
specify { expect(subject).to eql("/#{project.namespace.path}/#{project.name}/-/blob/#{project.default_branch_or_master}/#{lockfile}") }
end
end end
end end
...@@ -58,8 +58,8 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -58,8 +58,8 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
let(:apache_license) { report.by_license_name('Apache 2.0') } let(:apache_license) { report.by_license_name('Apache 2.0') }
before do before do
mit_license.add_dependency('Library4') mit_license.add_dependency(name: 'Library4')
apache_license.add_dependency('Library3') apache_license.add_dependency(name: 'Library3')
subject subject
end end
...@@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
before do before do
report report
.add_license(id: nil, name: 'MIT') .add_license(id: nil, name: 'MIT')
.add_dependency('rails') .add_dependency(name: 'rails')
end end
context 'when a denied license is found in the report' do context 'when a denied license is found in the report' do
...@@ -182,7 +182,7 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -182,7 +182,7 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
before do before do
base_report base_report
.add_license(id: 'MIT', name: 'MIT License') .add_license(id: 'MIT', name: 'MIT License')
.add_dependency('rails') .add_dependency(name: 'rails')
end end
specify do specify do
...@@ -197,13 +197,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -197,13 +197,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
let(:head_report) { build(:license_scan_report, :version_1) } let(:head_report) { build(:license_scan_report, :version_1) }
before do before do
base_report.add_license(id: nil, name: 'MIT').add_dependency('Library1') base_report.add_license(id: nil, name: 'MIT').add_dependency(name: 'Library1')
base_report.add_license(id: nil, name: 'BSD').add_dependency('Library1') base_report.add_license(id: nil, name: 'BSD').add_dependency(name: 'Library1')
base_report.add_license(id: nil, name: 'WTFPL').add_dependency('Library2') base_report.add_license(id: nil, name: 'WTFPL').add_dependency(name: 'Library2')
head_report.add_license(id: nil, name: 'MIT').add_dependency('Library1') head_report.add_license(id: nil, name: 'MIT').add_dependency(name: 'Library1')
head_report.add_license(id: nil, name: 'Apache 2.0').add_dependency('Library3') head_report.add_license(id: nil, name: 'Apache 2.0').add_dependency(name: 'Library3')
head_report.add_license(id: nil, name: 'bsd').add_dependency('Library1') head_report.add_license(id: nil, name: 'bsd').add_dependency(name: 'Library1')
end end
it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') } it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') }
...@@ -216,13 +216,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -216,13 +216,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
let(:head_report) { build(:license_scan_report, :version_2) } let(:head_report) { build(:license_scan_report, :version_2) }
before do before do
base_report.add_license(id: 'MIT', name: 'MIT').add_dependency('Library1') base_report.add_license(id: 'MIT', name: 'MIT').add_dependency(name: 'Library1')
base_report.add_license(id: 'BSD-3-Clause', name: 'BSD').add_dependency('Library1') base_report.add_license(id: 'BSD-3-Clause', name: 'BSD').add_dependency(name: 'Library1')
base_report.add_license(id: 'WTFPL', name: 'WTFPL').add_dependency('Library2') base_report.add_license(id: 'WTFPL', name: 'WTFPL').add_dependency(name: 'Library2')
head_report.add_license(id: 'BSD-3-Clause', name: 'bsd').add_dependency('Library1') head_report.add_license(id: 'BSD-3-Clause', name: 'bsd').add_dependency(name: 'Library1')
head_report.add_license(id: 'Apache-2.0', name: 'Apache 2.0').add_dependency('Library3') head_report.add_license(id: 'Apache-2.0', name: 'Apache 2.0').add_dependency(name: 'Library3')
head_report.add_license(id: 'MIT', name: 'MIT License').add_dependency('Library1') head_report.add_license(id: 'MIT', name: 'MIT License').add_dependency(name: 'Library1')
end end
it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') } it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') }
...@@ -235,13 +235,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -235,13 +235,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
let(:head_report) { build(:license_scan_report, :version_2) } let(:head_report) { build(:license_scan_report, :version_2) }
before do before do
base_report.add_license(id: nil, name: 'MIT').add_dependency('Library1') base_report.add_license(id: nil, name: 'MIT').add_dependency(name: 'Library1')
base_report.add_license(id: nil, name: 'BSD').add_dependency('Library1') base_report.add_license(id: nil, name: 'BSD').add_dependency(name: 'Library1')
base_report.add_license(id: nil, name: 'WTFPL').add_dependency('Library2') base_report.add_license(id: nil, name: 'WTFPL').add_dependency(name: 'Library2')
head_report.add_license(id: 'BSD-3-Clause', name: 'bsd').add_dependency('Library1') head_report.add_license(id: 'BSD-3-Clause', name: 'bsd').add_dependency(name: 'Library1')
head_report.add_license(id: 'Apache-2.0', name: 'Apache 2.0').add_dependency('Library3') head_report.add_license(id: 'Apache-2.0', name: 'Apache 2.0').add_dependency(name: 'Library3')
head_report.add_license(id: 'MIT', name: 'MIT').add_dependency('Library1') head_report.add_license(id: 'MIT', name: 'MIT').add_dependency(name: 'Library1')
end end
it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') } it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') }
...@@ -254,13 +254,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do ...@@ -254,13 +254,13 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::Report do
let(:head_report) { build(:license_scan_report, :version_1) } let(:head_report) { build(:license_scan_report, :version_1) }
before do before do
base_report.add_license(id: 'MIT', name: 'MIT').add_dependency('Library1') base_report.add_license(id: 'MIT', name: 'MIT').add_dependency(name: 'Library1')
base_report.add_license(id: 'BSD-3-Clause', name: 'BSD').add_dependency('Library1') base_report.add_license(id: 'BSD-3-Clause', name: 'BSD').add_dependency(name: 'Library1')
base_report.add_license(id: 'WTFPL', name: 'WTFPL').add_dependency('Library2') base_report.add_license(id: 'WTFPL', name: 'WTFPL').add_dependency(name: 'Library2')
head_report.add_license(id: nil, name: 'bsd').add_dependency('Library1') head_report.add_license(id: nil, name: 'bsd').add_dependency(name: 'Library1')
head_report.add_license(id: nil, name: 'Apache 2.0').add_dependency('Library3') head_report.add_license(id: nil, name: 'Apache 2.0').add_dependency(name: 'Library3')
head_report.add_license(id: nil, name: 'MIT').add_dependency('Library1') head_report.add_license(id: nil, name: 'MIT').add_dependency(name: 'Library1')
end end
it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') } it { expect(names_from(subject[:added])).to contain_exactly('Apache 2.0') }
......
...@@ -8,8 +8,8 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::ReportsComparer do ...@@ -8,8 +8,8 @@ RSpec.describe Gitlab::Ci::Reports::LicenseScanning::ReportsComparer do
let(:report_comparer) { described_class.new(report_1, report_2) } let(:report_comparer) { described_class.new(report_1, report_2) }
before do before do
report_1.add_license(id: nil, name: 'BSD').add_dependency('Library1') report_1.add_license(id: nil, name: 'BSD').add_dependency(name: 'Library1')
report_2.add_license(id: nil, name: 'bsd').add_dependency('Library1') report_2.add_license(id: nil, name: 'bsd').add_dependency(name: 'Library1')
end end
def names_from(licenses) def names_from(licenses)
......
...@@ -12,7 +12,7 @@ RSpec.describe LicenseEntity do ...@@ -12,7 +12,7 @@ RSpec.describe LicenseEntity do
let(:path) { 'some_path' } let(:path) { 'some_path' }
before do before do
license.add_dependency('rails') license.add_dependency(name: 'rails')
allow(license.dependencies.first).to receive(:path).and_return(path) allow(license.dependencies.first).to receive(:path).and_return(path)
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Security::LicensePolicyEntity do RSpec.describe Security::LicensePolicyEntity do
let(:license) { build(:license_scanning_license, :mit).tap { |x| x.add_dependency('rails') } } let(:license) { build(:license_scanning_license, :mit).tap { |x| x.add_dependency(name: 'rails') } }
let(:policy) { build(:software_license_policy, :allowed) } let(:policy) { build(:software_license_policy, :allowed) }
let(:entity) { described_class.new(SCA::LicensePolicy.new(license, policy)) } let(:entity) { described_class.new(SCA::LicensePolicy.new(license, policy)) }
......
...@@ -5,7 +5,9 @@ module LicenseScanningReportHelper ...@@ -5,7 +5,9 @@ module LicenseScanningReportHelper
Gitlab::Ci::Reports::LicenseScanning::Report.new.tap do |report| Gitlab::Ci::Reports::LicenseScanning::Report.new.tap do |report|
dependencies.each do |license_name, dependencies| dependencies.each do |license_name, dependencies|
dependencies.each do |dependency_name| dependencies.each do |dependency_name|
report.add_license(id: nil, name: license_name.to_s, url: "https://opensource.org/licenses/license1").add_dependency(dependency_name) report
.add_license(id: nil, name: license_name.to_s, url: "https://opensource.org/licenses/license1")
.add_dependency(name: dependency_name)
end end
end end
end end
...@@ -33,12 +35,12 @@ module LicenseScanningReportHelper ...@@ -33,12 +35,12 @@ module LicenseScanningReportHelper
def create_license def create_license
Gitlab::Ci::Reports::LicenseScanning::License.new(id: nil, name: 'License1', url: "https://opensource.org/licenses/license1").tap do |license| Gitlab::Ci::Reports::LicenseScanning::License.new(id: nil, name: 'License1', url: "https://opensource.org/licenses/license1").tap do |license|
license.add_dependency('Dependency1') license.add_dependency(name: 'Dependency1')
license.add_dependency('Dependency2') license.add_dependency(name: 'Dependency2')
end end
end end
def create_dependency def create_dependency
Gitlab::Ci::Reports::LicenseScanning::Dependency.new('Dependency1') Gitlab::Ci::Reports::LicenseScanning::Dependency.new(name: 'Dependency1')
end end
end end
...@@ -48,62 +48,45 @@ module QA ...@@ -48,62 +48,45 @@ module QA
mr.file_content = mr.file_content =
<<~FILE_UPDATE <<~FILE_UPDATE
{ {
"version": "2.1",
"licenses": [ "licenses": [
{ {
"count": 1, "id": "WTFPL",
"name": "WTFPL" "name": "Do What The F*ck You Want To Public License",
"url": "http://www.wtfpl.net/about/"
}, },
{ {
"count": 1, "id": "MIT",
"name": "MIT" "name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
}, },
{ {
"count": 1, "id": "Zlib",
"name": "Zlib" "name": "zlib License",
"url": "https://opensource.org/licenses/Zlib"
} }
], ],
"dependencies": [ "dependencies": [
{ {
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/mit-license"
},
"dependency": {
"name": "actioncable", "name": "actioncable",
"url": "http://rubyonrails.org", "version": "6.0.3.3",
"description": "WebSocket framework for Rails.", "package_manager": "bundler",
"paths": [ "path": "Gemfile.lock",
"." "licenses": ["MIT"]
]
}
}, },
{ {
"license": {
"name": "WTFPL",
"url": "http://www.wtfpl.net/"
},
"dependency": {
"name": "wtfpl_init", "name": "wtfpl_init",
"url": "https://rubygems.org/gems/wtfpl_init", "version": "0.1.0",
"description": "Download WTFPL license file and rename to LICENSE.md or something", "package_manager": "bundler",
"paths": [ "path": "Gemfile.lock",
"." "licenses": ["WTFPL"]
]
}
}, },
{ {
"license": {
"name": "Zlib", "name": "Zlib",
"url": "https://www.zlib.net/" "version": "1.2.11",
}, "package_manager": "bundler",
"dependency": { "path": "Gemfile.lock",
"name": "zlib", "licenses": ["Zlib"]
"url": "https://www.zlib.net/",
"description": "Ruby interface for the zlib compression/decompression library",
"paths": [
"."
]
}
} }
] ]
} }
......
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