From e39dbbfd1815607b7406c4b6f694f99d330e952b Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Mon, 2 Jun 2025 18:17:51 +0200 Subject: [PATCH 01/26] Add Gitlab::Utils::MimeType#from_filename --- lib/gitlab/utils/mime_type.rb | 11 +++++++++++ spec/lib/gitlab/utils/mime_type_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/gitlab/utils/mime_type.rb b/lib/gitlab/utils/mime_type.rb index d82092a2d2c12d..3a823ffbd8cccd 100644 --- a/lib/gitlab/utils/mime_type.rb +++ b/lib/gitlab/utils/mime_type.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require 'magic' +require 'mime/types' # This wraps calls to a gem which support mime type detection. # We use the `ruby-magic` gem instead of `mimemagic` due to licensing issues @@ -19,6 +20,16 @@ def from_string(string) string.type end + + def from_filename(filename) + return unless filename.is_a?(String) + + types = ::MIME::Types.type_for(filename) + + return unless types.present? + + types.first.content_type + end end end end diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index e8b369362243cc..32f7d3818c8bc3 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -54,4 +54,28 @@ it { is_expected.to eq('text/plain') } end end + + describe ".from_filename" do + subject { described_class.from_filename(filename) } + + context 'when filename is not a string' do + let(:filename) { 1 } + + it { is_expected.to be_nil } + end + + context "when filename is not recognizable" do + let(:filename) { 'filename.tf' } + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when filname is recognizable" do + let(:filename) { "test.css" } + + it { is_expected.to eq('text/css') } + end + end end -- GitLab From e2e323c67d2ec513fc9a668b6c543d9985a92c7e Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Mon, 2 Jun 2025 18:32:12 +0200 Subject: [PATCH 02/26] Use #from_filename in SendFileUpload concern --- app/controllers/concerns/send_file_upload.rb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 5e44679dbfa8ff..6d9bb4ac722c0d 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -44,17 +44,7 @@ def send_upload( def content_type_for(attachment) return '' unless attachment - guess_content_type(attachment) - end - - def guess_content_type(filename) - types = MIME::Types.type_for(filename) - - if types.present? - types.first.content_type - else - "application/octet-stream" - end + Gitlab::Utils::MimeType.from_filename(attachment) || 'application/octet-stream' end private -- GitLab From 2f0f8dae119744fa367c7eaee929597b9f03c614 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 3 Jun 2025 12:52:20 +0200 Subject: [PATCH 03/26] Create packages_generic_package_content_type feature flag --- .../beta/packages_generic_package_content_type.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config/feature_flags/beta/packages_generic_package_content_type.yml diff --git a/config/feature_flags/beta/packages_generic_package_content_type.yml b/config/feature_flags/beta/packages_generic_package_content_type.yml new file mode 100644 index 00000000000000..01f11191d647cf --- /dev/null +++ b/config/feature_flags/beta/packages_generic_package_content_type.yml @@ -0,0 +1,10 @@ +--- +name: packages_generic_package_content_type +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444768 +introduced_by_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547424 +milestone: '18.1' +group: group::package registry +type: beta +default_enabled: false -- GitLab From 67ad5d7e3014aaf90dff4cb286ca17d211e029bf Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 3 Jun 2025 17:36:44 +0200 Subject: [PATCH 04/26] Guess content type when get generic package file --- lib/api/generic_packages.rb | 14 +++++++++++++- lib/api/helpers/packages_helpers.rb | 10 ++++++++-- spec/requests/api/generic_packages_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 91071ec0781751..1b9544f7cbe3dc 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -21,6 +21,17 @@ class GenericPackages < ::API::Base requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end + helpers do + def guess_content_type(package_file) + return unless Feature.enabled?(:packages_generic_package_content_type, authorized_user_project) + + type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) + type = 'text/plain' if package_file.file.file_storage? && (File.extname(package_file.file_name) == '.js') + + type || 'application/octet-stream' + end + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true @@ -151,7 +162,8 @@ class GenericPackages < ::API::Base track_package_event('pull_package', :generic, project: project, namespace: project.namespace) - present_package_file!(package_file, content_disposition: :attachment) + content_type = guess_content_type(package_file) + present_package_file!(package_file, content_disposition: :attachment, content_type: content_type) end end end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index a7b2795253a56e..325f7e96c01793 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -137,14 +137,20 @@ def track_package_event(action, scope, **args) end end - def present_package_file!(package_file, supports_direct_download: true, content_disposition: nil) + # rubocop: disable Layout/LineLength -- to avoid parameters on the next line + def present_package_file!(package_file, supports_direct_download: true, content_disposition: nil, content_type: nil) + return head :not_found unless package_file.file.exists? + package_file.package.touch_last_downloaded_at + present_carrierwave_file!( package_file.file, supports_direct_download: supports_direct_download, - content_disposition: content_disposition + content_disposition: content_disposition, + content_type: content_type ) end + # rubocop: enable Layout/LineLength def protect_package!(package_name, package_type) service_response = diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 87f9d251094d79..d1959b33813e2e 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -1111,6 +1111,7 @@ def upload_file( before do project.add_developer(user) + stub_feature_flags(packages_generic_package_content_type: false) end context 'when direct download is enabled' do @@ -1148,6 +1149,27 @@ def upload_file( expect(response).to have_gitlab_http_status(:ok) end + + context 'when feature :packages_generic_package_content_type is enabled' do + before do + stub_feature_flags(packages_generic_package_content_type: true) + end + + it 'sends a file with response-content-disposition, response-content-type and filename' do + expect(::Gitlab::Workhorse).to receive(:send_url) + .with(instance_of(String), { + response_headers: { + 'Content-Disposition' => dispositon_header, + 'Content-Type' => 'application/gzip' + } + } + ).and_call_original + + download + + expect(response).to have_gitlab_http_status(:ok) + end + end end end -- GitLab From 51d157374b09ec643ac95bc060ef6a3e42c08051 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 3 Jun 2025 18:22:47 +0200 Subject: [PATCH 05/26] Rename guess_content_type to detect_content_type --- lib/api/generic_packages.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 1b9544f7cbe3dc..f029595b2f2a81 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -22,7 +22,7 @@ class GenericPackages < ::API::Base end helpers do - def guess_content_type(package_file) + def detect_content_type(package_file) return unless Feature.enabled?(:packages_generic_package_content_type, authorized_user_project) type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) @@ -162,7 +162,7 @@ def guess_content_type(package_file) track_package_event('pull_package', :generic, project: project, namespace: project.namespace) - content_type = guess_content_type(package_file) + content_type = detect_content_type(package_file) present_package_file!(package_file, content_disposition: :attachment, content_type: content_type) end end -- GitLab From 3a786d6e53c5ddf0cba281a87ab50360aa48df3f Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Wed, 4 Jun 2025 10:46:49 +0200 Subject: [PATCH 06/26] Add comment --- lib/api/generic_packages.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index f029595b2f2a81..2d3a75fe943a33 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -26,6 +26,9 @@ def detect_content_type(package_file) return unless Feature.enabled?(:packages_generic_package_content_type, authorized_user_project) type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) + + # Content-type `text/javascript` will trigger Rails' cross-origin JavaScript protection. + # Therefore we return `text/plain` for JavaScript files. type = 'text/plain' if package_file.file.file_storage? && (File.extname(package_file.file_name) == '.js') type || 'application/octet-stream' -- GitLab From b0a8326b7b2c1d122407bf192725ec6083e7f3d8 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Wed, 4 Jun 2025 08:53:49 +0000 Subject: [PATCH 07/26] Add feature flag `introduced_by_url` --- .../beta/packages_generic_package_content_type.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/beta/packages_generic_package_content_type.yml b/config/feature_flags/beta/packages_generic_package_content_type.yml index 01f11191d647cf..f320ee398e4dbe 100644 --- a/config/feature_flags/beta/packages_generic_package_content_type.yml +++ b/config/feature_flags/beta/packages_generic_package_content_type.yml @@ -2,7 +2,7 @@ name: packages_generic_package_content_type description: feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444768 -introduced_by_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193331 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547424 milestone: '18.1' group: group::package registry -- GitLab From 20a693441b0e7a347e0d6162da81f94f45819b47 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Wed, 4 Jun 2025 10:55:05 +0200 Subject: [PATCH 08/26] Add feature flag description --- .../beta/packages_generic_package_content_type.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/beta/packages_generic_package_content_type.yml b/config/feature_flags/beta/packages_generic_package_content_type.yml index f320ee398e4dbe..907b3ea844ec01 100644 --- a/config/feature_flags/beta/packages_generic_package_content_type.yml +++ b/config/feature_flags/beta/packages_generic_package_content_type.yml @@ -1,6 +1,6 @@ --- name: packages_generic_package_content_type -description: +description: Return generic package file with corresponding content type feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444768 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193331 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547424 -- GitLab From 3963575ea3df260791e81d5b604b2baa912c18af Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Wed, 4 Jun 2025 12:47:08 +0200 Subject: [PATCH 09/26] Fix spec by passing project as parameter --- lib/api/generic_packages.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 2d3a75fe943a33..3e570c6e332671 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -22,8 +22,8 @@ class GenericPackages < ::API::Base end helpers do - def detect_content_type(package_file) - return unless Feature.enabled?(:packages_generic_package_content_type, authorized_user_project) + def detect_content_type(package_file, project) + return unless Feature.enabled?(:packages_generic_package_content_type, project) type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) @@ -165,7 +165,7 @@ def detect_content_type(package_file) track_package_event('pull_package', :generic, project: project, namespace: project.namespace) - content_type = detect_content_type(package_file) + content_type = detect_content_type(package_file, project) present_package_file!(package_file, content_disposition: :attachment, content_type: content_type) end end -- GitLab From de0e8de6881be5bda52e7265aa3fccb6dfca0642 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Fri, 6 Jun 2025 12:42:57 +0200 Subject: [PATCH 10/26] Change feature flag type to gitlab_com_derisk --- .../packages_generic_package_content_type.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename config/feature_flags/{beta => gitlab_com_derisk}/packages_generic_package_content_type.yml (94%) diff --git a/config/feature_flags/beta/packages_generic_package_content_type.yml b/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml similarity index 94% rename from config/feature_flags/beta/packages_generic_package_content_type.yml rename to config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml index 907b3ea844ec01..f5105c155c1cd5 100644 --- a/config/feature_flags/beta/packages_generic_package_content_type.yml +++ b/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml @@ -6,5 +6,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193331 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547424 milestone: '18.1' group: group::package registry -type: beta +type: gitlab_com_derisk default_enabled: false -- GitLab From f37e82309e28e39970c06d5d4bf0f13586fc32c8 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Fri, 6 Jun 2025 12:51:02 +0200 Subject: [PATCH 11/26] Use not_found! helper --- lib/api/helpers/packages_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 325f7e96c01793..e590d56cad35f9 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -139,7 +139,7 @@ def track_package_event(action, scope, **args) # rubocop: disable Layout/LineLength -- to avoid parameters on the next line def present_package_file!(package_file, supports_direct_download: true, content_disposition: nil, content_type: nil) - return head :not_found unless package_file.file.exists? + not_found!('Package file') unless package_file.file.exists? package_file.package.touch_last_downloaded_at -- GitLab From bccd3d0ea4e073ba118ea08e6ad65d1936ce7078 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Fri, 6 Jun 2025 13:48:13 +0200 Subject: [PATCH 12/26] Supplement tests --- spec/requests/api/generic_packages_spec.rb | 46 +++++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index d1959b33813e2e..c43d9a97c90b8a 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -1111,7 +1111,6 @@ def upload_file( before do project.add_developer(user) - stub_feature_flags(packages_generic_package_content_type: false) end context 'when direct download is enabled' do @@ -1119,16 +1118,33 @@ def upload_file( "response-content-disposition=attachment%3B%20filename%3D%22#{package_file.file_name}" end + let(:content_type_param) { "response-content-type=application%2Fgzip" } + before do stub_package_file_object_storage end - it 'includes response-content-disposition and filename in the redirect file URL' do + it 'includes response-content-disposition, response-content-type and filename in the redirect file URL' do download expect(response.parsed_body).to include(disposition_param) + expect(response.parsed_body).to include(content_type_param) expect(response).to have_gitlab_http_status(:redirect) end + + context 'when feature :packages_generic_package_content_type is disabled' do + before do + stub_feature_flags(packages_generic_package_content_type: false) + end + + it 'includes response-content-disposition and filename in the redirect file URL' do + download + + expect(response.parsed_body).to include(disposition_param) + expect(response.parsed_body).not_to include(content_type_param) + expect(response).to have_gitlab_http_status(:redirect) + end + end end context 'when direct download is disabled' do @@ -1140,30 +1156,30 @@ def upload_file( stub_package_file_object_storage(proxy_download: true) end - it 'sends a file with response-content-disposition and filename' do + it 'sends a file with response-content-disposition, response-content-type and filename' do expect(::Gitlab::Workhorse).to receive(:send_url) - .with(instance_of(String), { response_headers: { 'Content-Disposition' => dispositon_header } }) - .and_call_original + .with(instance_of(String), { + response_headers: { + 'Content-Disposition' => dispositon_header, + 'Content-Type' => 'application/gzip' + } + } + ).and_call_original download expect(response).to have_gitlab_http_status(:ok) end - context 'when feature :packages_generic_package_content_type is enabled' do + context 'when feature :packages_generic_package_content_type is disabled' do before do - stub_feature_flags(packages_generic_package_content_type: true) + stub_feature_flags(packages_generic_package_content_type: false) end - it 'sends a file with response-content-disposition, response-content-type and filename' do + it 'sends a file with response-content-disposition and filename' do expect(::Gitlab::Workhorse).to receive(:send_url) - .with(instance_of(String), { - response_headers: { - 'Content-Disposition' => dispositon_header, - 'Content-Type' => 'application/gzip' - } - } - ).and_call_original + .with(instance_of(String), { response_headers: { 'Content-Disposition' => dispositon_header } }) + .and_call_original download -- GitLab From 9a1dcef2992bae5871d14b9602c5d0f46e7a83dd Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Fri, 6 Jun 2025 13:54:25 +0200 Subject: [PATCH 13/26] Move method to be along side with the other helper methods --- lib/api/generic_packages.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 3e570c6e332671..5018975487ff2d 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -21,20 +21,6 @@ class GenericPackages < ::API::Base requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end - helpers do - def detect_content_type(package_file, project) - return unless Feature.enabled?(:packages_generic_package_content_type, project) - - type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) - - # Content-type `text/javascript` will trigger Rails' cross-origin JavaScript protection. - # Therefore we return `text/plain` for JavaScript files. - type = 'text/plain' if package_file.file.file_storage? && (File.extname(package_file.file_name) == '.js') - - type || 'application/octet-stream' - end - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true @@ -194,6 +180,18 @@ def encoded_file_name file_name = [declared_params[:path], declared_params[:file_name]].compact.join('/') declared_params[:path].present? ? URI.encode_uri_component(file_name) : file_name end + + def detect_content_type(package_file, project) + return unless Feature.enabled?(:packages_generic_package_content_type, project) + + type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) + + # Content-type `text/javascript` will trigger Rails' cross-origin JavaScript protection. + # Therefore we return `text/plain` for JavaScript files. + type = 'text/plain' if package_file.file.file_storage? && (File.extname(package_file.file_name) == '.js') + + type || 'application/octet-stream' + end end end end -- GitLab From 6803f8bbfdb9378cf276a47c5bccf53c84a70a8b Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 11:54:41 +0200 Subject: [PATCH 14/26] Unify double quote to single quote --- spec/lib/gitlab/utils/mime_type_spec.rb | 52 ++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index 32f7d3818c8bc3..a50123613ba628 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -1,61 +1,61 @@ # frozen_string_literal: true -require "fast_spec_helper" -require "rspec/parameterized" +require 'fast_spec_helper' +require 'rspec/parameterized' RSpec.describe Gitlab::Utils::MimeType do - describe ".from_io" do + describe '.from_io' do subject { described_class.from_io(io) } - context "input isn't an IO" do - let(:io) { "test" } + context 'input is not an IO' do + let(:io) { 'test' } - it "returns nil" do + it 'returns nil' do expect(subject).to be_nil end end - context "input is a file" do + context 'input is a file' do using RSpec::Parameterized::TableSyntax where(:fixture, :mime_type) do - "banana_sample.gif" | "image/gif" - "rails_sample.jpg" | "image/jpeg" - "rails_sample.png" | "image/png" - "rails_sample.bmp" | "image/bmp" - "rails_sample.tif" | "image/tiff" - "sample.ico" | "image/vnd.microsoft.icon" - "sample_doc.md" | "text/plain" - "csv_empty.csv" | "application/x-empty" + 'banana_sample.gif' | 'image/gif' + 'rails_sample.jpg' | 'image/jpeg' + 'rails_sample.png' | 'image/png' + 'rails_sample.bmp' | 'image/bmp' + 'rails_sample.tif' | 'image/tiff' + 'sample.ico' | 'image/vnd.microsoft.icon' + 'sample_doc.md' | 'text/plain' + 'csv_empty.csv' | 'application/x-empty' end with_them do - let(:io) { File.open(File.join(__dir__, "../../../fixtures", fixture)) } + let(:io) { File.open(File.join(__dir__, '../../../fixtures', fixture)) } it { is_expected.to eq(mime_type) } end end end - describe ".from_string" do + describe '.from_string' do subject { described_class.from_string(str) } - context "input isn't a string" do + context 'input is not a string' do let(:str) { nil } - it "returns nil" do + it 'returns nil' do expect(subject).to be_nil end end - context "input is a string" do - let(:str) { "plain text" } + context 'input is a string' do + let(:str) { 'plain text' } it { is_expected.to eq('text/plain') } end end - describe ".from_filename" do + describe '.from_filename' do subject { described_class.from_filename(filename) } context 'when filename is not a string' do @@ -64,16 +64,16 @@ it { is_expected.to be_nil } end - context "when filename is not recognizable" do + context 'when filename is not recognizable' do let(:filename) { 'filename.tf' } - it "returns nil" do + it 'returns nil' do expect(subject).to be_nil end end - context "when filname is recognizable" do - let(:filename) { "test.css" } + context 'when filname is recognizable' do + let(:filename) { 'test.css' } it { is_expected.to eq('text/css') } end -- GitLab From 226192c09457e5a8ed46f30c25967353e5682c17 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 12:01:05 +0200 Subject: [PATCH 15/26] Update tests to use 1 line syntax --- spec/lib/gitlab/utils/mime_type_spec.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index a50123613ba628..3253c224556f6b 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -43,9 +43,7 @@ context 'input is not a string' do let(:str) { nil } - it 'returns nil' do - expect(subject).to be_nil - end + it { is_expected.to be_nil } end context 'input is a string' do @@ -67,9 +65,7 @@ context 'when filename is not recognizable' do let(:filename) { 'filename.tf' } - it 'returns nil' do - expect(subject).to be_nil - end + it { is_expected.to be_nil } end context 'when filname is recognizable' do -- GitLab From 442ea7bffb92b835e5a0c2446973e05dc9998807 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 12:03:51 +0200 Subject: [PATCH 16/26] Use single quote --- spec/requests/api/generic_packages_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index c43d9a97c90b8a..453d9fdb258148 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -1118,7 +1118,7 @@ def upload_file( "response-content-disposition=attachment%3B%20filename%3D%22#{package_file.file_name}" end - let(:content_type_param) { "response-content-type=application%2Fgzip" } + let(:content_type_param) { 'response-content-type=application%2Fgzip' } before do stub_package_file_object_storage -- GitLab From ece43913eb13eb8c1c63a182217a0f4b55ecd65a Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 12:08:35 +0200 Subject: [PATCH 17/26] Separate tests into 2 cases --- spec/requests/api/generic_packages_spec.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 453d9fdb258148..7a43ffe8562e9a 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -1137,11 +1137,16 @@ def upload_file( stub_feature_flags(packages_generic_package_content_type: false) end + it 'does not include content type param' do + download + + expect(response.parsed_body).not_to include(content_type_param) + end + it 'includes response-content-disposition and filename in the redirect file URL' do download expect(response.parsed_body).to include(disposition_param) - expect(response.parsed_body).not_to include(content_type_param) expect(response).to have_gitlab_http_status(:redirect) end end @@ -1176,6 +1181,16 @@ def upload_file( stub_feature_flags(packages_generic_package_content_type: false) end + it 'does not send file with Content-Type' do + expect(::Gitlab::Workhorse).to receive(:send_url) + .with(instance_of(String), { response_headers: hash_not_including('Content-Type') }) + .and_call_original + + download + + expect(response).to have_gitlab_http_status(:ok) + end + it 'sends a file with response-content-disposition and filename' do expect(::Gitlab::Workhorse).to receive(:send_url) .with(instance_of(String), { response_headers: { 'Content-Disposition' => dispositon_header } }) -- GitLab From 621ffee6562b21090402524c2b1aa72ab9db452f Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 12:15:08 +0200 Subject: [PATCH 18/26] Remove unnecessary condition --- lib/api/helpers/packages_helpers.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index e590d56cad35f9..ae212e5e0af9e4 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -139,8 +139,6 @@ def track_package_event(action, scope, **args) # rubocop: disable Layout/LineLength -- to avoid parameters on the next line def present_package_file!(package_file, supports_direct_download: true, content_disposition: nil, content_type: nil) - not_found!('Package file') unless package_file.file.exists? - package_file.package.touch_last_downloaded_at present_carrierwave_file!( -- GitLab From 192ed0829c8540969fd50168f71edebbcc2b6f84 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 10 Jun 2025 12:21:51 +0200 Subject: [PATCH 19/26] Shorten syntax --- spec/requests/api/generic_packages_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 7a43ffe8562e9a..338c9854c061e3 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -1127,8 +1127,7 @@ def upload_file( it 'includes response-content-disposition, response-content-type and filename in the redirect file URL' do download - expect(response.parsed_body).to include(disposition_param) - expect(response.parsed_body).to include(content_type_param) + expect(response.parsed_body).to include(content_type_param, content_type_param) expect(response).to have_gitlab_http_status(:redirect) end -- GitLab From 04cd30db244b9642857dee0b7e084bb84b413338 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Thu, 12 Jun 2025 19:14:41 +0200 Subject: [PATCH 20/26] Rewrite tests as table-based tests --- spec/lib/gitlab/utils/mime_type_spec.rb | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index 3253c224556f6b..67a926eb1cf16c 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -54,24 +54,19 @@ end describe '.from_filename' do - subject { described_class.from_filename(filename) } + using RSpec::Parameterized::TableSyntax - context 'when filename is not a string' do - let(:filename) { 1 } - - it { is_expected.to be_nil } - end - - context 'when filename is not recognizable' do - let(:filename) { 'filename.tf' } - - it { is_expected.to be_nil } + where(:filename, :mime_type) do + 1 | nil + 'test.tf' | nil + 'test.css' | 'text/css' + 'test.js' | 'text/javascript' end - context 'when filname is recognizable' do - let(:filename) { 'test.css' } + with_them do + subject { described_class.from_filename(filename) } - it { is_expected.to eq('text/css') } + it { is_expected.to eq(mime_type) } end end end -- GitLab From 17194e9e951dec6fd94b674a97d18332cd81e3c2 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Mon, 16 Jun 2025 15:12:33 +0200 Subject: [PATCH 21/26] Add prefix to Gitlab::Utils::MimeType reference --- app/controllers/concerns/send_file_upload.rb | 2 +- app/controllers/projects/jobs_controller.rb | 2 +- app/services/lfs/file_transformer.rb | 2 +- app/uploaders/content_type_whitelist.rb | 2 +- lib/api/generic_packages.rb | 2 +- lib/gitlab/sanitizers/exif.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 6d9bb4ac722c0d..955273da791bac 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -44,7 +44,7 @@ def send_upload( def content_type_for(attachment) return '' unless attachment - Gitlab::Utils::MimeType.from_filename(attachment) || 'application/octet-stream' + ::Gitlab::Utils::MimeType.from_filename(attachment) || 'application/octet-stream' end private diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 43a09150b70ea3..75eca566b4e802 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -265,7 +265,7 @@ def build_path(build) end def raw_trace_content_disposition(raw_data) - mime_type = Gitlab::Utils::MimeType.from_string(raw_data) + mime_type = ::Gitlab::Utils::MimeType.from_string(raw_data) # if mime_type is nil can also represent 'text/plain' return 'inline' if mime_type.nil? || mime_type == 'text/plain' diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb index ef227b6e20f732..477c17dcecea0d 100644 --- a/app/services/lfs/file_transformer.rb +++ b/app/services/lfs/file_transformer.rb @@ -72,7 +72,7 @@ def cached_attributes # rubocop: disable CodeReuse/ActiveRecord def create_lfs_object!(lfs_pointer_file, file_content, detect_content_type) LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object| - lfs_object.file = if detect_content_type && (content_type = Gitlab::Utils::MimeType.from_string(file_content)) + lfs_object.file = if detect_content_type && (content_type = ::Gitlab::Utils::MimeType.from_string(file_content)) CarrierWaveStringFile.new_file( file_content: file_content, filename: '', diff --git a/app/uploaders/content_type_whitelist.rb b/app/uploaders/content_type_whitelist.rb index 82c6b9b3a6114d..4c9d180dab4966 100644 --- a/app/uploaders/content_type_whitelist.rb +++ b/app/uploaders/content_type_whitelist.rb @@ -43,7 +43,7 @@ def whitelisted_content_type?(content_type) def mime_magic_content_type(path) if path File.open(path) do |file| - Gitlab::Utils::MimeType.from_io(file) || 'invalid/invalid' + ::Gitlab::Utils::MimeType.from_io(file) || 'invalid/invalid' end end rescue Errno::ENOENT diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 5018975487ff2d..c9cbe925501f1c 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -184,7 +184,7 @@ def encoded_file_name def detect_content_type(package_file, project) return unless Feature.enabled?(:packages_generic_package_content_type, project) - type = Gitlab::Utils::MimeType.from_filename(package_file.file_name) + type = ::Gitlab::Utils::MimeType.from_filename(package_file.file_name) # Content-type `text/javascript` will trigger Rails' cross-origin JavaScript protection. # Therefore we return `text/plain` for JavaScript files. diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb index dee9e5f3d0e42f..08fc9f90ac0ed9 100644 --- a/lib/gitlab/sanitizers/exif.rb +++ b/lib/gitlab/sanitizers/exif.rb @@ -169,7 +169,7 @@ def fetch_upload_to_file(uploader, dir) end def check_for_allowed_types(contents, raise_error: true) - mime_type = Gitlab::Utils::MimeType.from_string(contents) + mime_type = ::Gitlab::Utils::MimeType.from_string(contents) allowed = ALLOWED_MIME_TYPES.include?(mime_type) if !allowed && raise_error -- GitLab From 335607cf6445c27eb7e4f84fe488272124590828 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Mon, 16 Jun 2025 15:54:40 +0200 Subject: [PATCH 22/26] Setup default value for #from_filename method --- lib/gitlab/utils/mime_type.rb | 6 ++--- spec/lib/gitlab/utils/mime_type_spec.rb | 32 ++++++++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/utils/mime_type.rb b/lib/gitlab/utils/mime_type.rb index 3a823ffbd8cccd..089be3fec7a838 100644 --- a/lib/gitlab/utils/mime_type.rb +++ b/lib/gitlab/utils/mime_type.rb @@ -21,12 +21,12 @@ def from_string(string) string.type end - def from_filename(filename) - return unless filename.is_a?(String) + def from_filename(filename, default: 'application/octet-stream') + return default unless filename.is_a?(String) types = ::MIME::Types.type_for(filename) - return unless types.present? + return default unless types.present? types.first.content_type end diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index 67a926eb1cf16c..fbeb8b2448b176 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -56,17 +56,33 @@ describe '.from_filename' do using RSpec::Parameterized::TableSyntax - where(:filename, :mime_type) do - 1 | nil - 'test.tf' | nil - 'test.css' | 'text/css' - 'test.js' | 'text/javascript' + context 'when default value is not given' do + where(:filename, :mime_type) do + 1 | nil + 'test.tf' | nil + 'test.css' | 'text/css' + 'test.js' | 'text/javascript' + end + + with_them do + subject { described_class.from_filename(filename) } + + it { is_expected.to eq(mime_type) } + end end - with_them do - subject { described_class.from_filename(filename) } + context 'when default value is given' do + where(:filename, :default, :mime_type) do + 1 | nil | nil + 'test.tf' | nil | nil + 'test.css' | 'text/plain' | 'text/css' + end + + with_them do + subject { described_class.from_filename(filename, default: default) } - it { is_expected.to eq(mime_type) } + it { is_expected.to eq(mime_type) } + end end end end -- GitLab From f2bddeab79f8e31ab2dbd5f41be867fc712694b1 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Mon, 16 Jun 2025 15:55:17 +0200 Subject: [PATCH 23/26] Remove fallback value from callers --- app/controllers/concerns/send_file_upload.rb | 2 +- lib/api/generic_packages.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 955273da791bac..0266adcee4f302 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -44,7 +44,7 @@ def send_upload( def content_type_for(attachment) return '' unless attachment - ::Gitlab::Utils::MimeType.from_filename(attachment) || 'application/octet-stream' + ::Gitlab::Utils::MimeType.from_filename(attachment) end private diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index c9cbe925501f1c..ffe6232b7685a4 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -190,7 +190,7 @@ def detect_content_type(package_file, project) # Therefore we return `text/plain` for JavaScript files. type = 'text/plain' if package_file.file.file_storage? && (File.extname(package_file.file_name) == '.js') - type || 'application/octet-stream' + type end end end -- GitLab From 17c63b8facaa21057132a68125cb7c91d4b82714 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 17 Jun 2025 13:14:00 +0200 Subject: [PATCH 24/26] Add test case --- spec/lib/gitlab/utils/mime_type_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index fbeb8b2448b176..03dfcfd86ab181 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -75,6 +75,7 @@ where(:filename, :default, :mime_type) do 1 | nil | nil 'test.tf' | nil | nil + 'test.tf' | 'text/plain' | 'text/plain' 'test.css' | 'text/plain' | 'text/css' end -- GitLab From 33fa00c97157c6fb31d96ed46ac0c0c9c9d962c3 Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 17 Jun 2025 13:14:30 +0200 Subject: [PATCH 25/26] Update feature flag milestone --- .../gitlab_com_derisk/packages_generic_package_content_type.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml b/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml index f5105c155c1cd5..1e8eea0897acb6 100644 --- a/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml +++ b/config/feature_flags/gitlab_com_derisk/packages_generic_package_content_type.yml @@ -4,7 +4,7 @@ description: Return generic package file with corresponding content type feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444768 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193331 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547424 -milestone: '18.1' +milestone: '18.2' group: group::package registry type: gitlab_com_derisk default_enabled: false -- GitLab From 4a235ccc3c91be048e2c2528924734d1f21565df Mon Sep 17 00:00:00 2001 From: Sylvia Shen Date: Tue, 17 Jun 2025 13:16:21 +0200 Subject: [PATCH 26/26] Fix spec --- spec/lib/gitlab/utils/mime_type_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/gitlab/utils/mime_type_spec.rb b/spec/lib/gitlab/utils/mime_type_spec.rb index 03dfcfd86ab181..a8473ea46bef94 100644 --- a/spec/lib/gitlab/utils/mime_type_spec.rb +++ b/spec/lib/gitlab/utils/mime_type_spec.rb @@ -58,8 +58,8 @@ context 'when default value is not given' do where(:filename, :mime_type) do - 1 | nil - 'test.tf' | nil + 1 | 'application/octet-stream' + 'test.tf' | 'application/octet-stream' 'test.css' | 'text/css' 'test.js' | 'text/javascript' end -- GitLab