discourse/lib/validators/upload_validator.rb
Guhyoun Nam 9c1812e071
FEATURE: add system_user_max_attachment_size_kb site setting (#28351)
* System user attachment size WIP

* spec check

* controller update

* add max to system_user_max_attachment_size_kb

* DEV: update to use static method for `max_attachment_size_for_user`

add test to use large image.
add check for failure.

* DEV: update `system_user_max_attachment_size_kb` default value to 0

remove unecessary test.
update tests to reflect the new default value of `system_user_max_attachment_size_kb`

* DEV: update maximum_file_size to check when is an attachment made by a system user

Add tests for when `system_user_max_attachment_size_kb` is over and under the limit
Add test for checking interaction with `max_attachment_size_kb`

* DEV: move `max_attachment_size_for_user` to private methods

* DEV: turn `max_attachment_size_for_user` into a static method

* DEV: typo in test case

* DEV: move max_attachment_size_for_user to private class method

* Revert "DEV: move max_attachment_size_for_user to private class method"

This reverts commit 5d5ae0b715.

---------

Co-authored-by: Gabriel Grubba <gabriel@discourse.org>
2024-08-16 11:03:39 -03:00

171 lines
4.8 KiB
Ruby

# frozen_string_literal: true
require "file_helper"
class UploadValidator < ActiveModel::Validator
def validate(upload)
# staff can upload any file in PM
if (upload.for_private_message && SiteSetting.allow_staff_to_upload_any_file_in_pm)
return true if upload.user&.staff?
end
# check the attachment blocklist
if upload.for_group_message && SiteSetting.allow_all_attachments_for_group_messages
return upload.original_filename =~ SiteSetting.blocked_attachment_filenames_regex
end
extension = File.extname(upload.original_filename)[1..-1] || ""
if upload.for_site_setting && upload.user&.staff? &&
FileHelper.is_supported_image?(upload.original_filename)
return true
end
if upload.for_gravatar && FileHelper.supported_gravatar_extensions.include?(extension)
maximum_image_file_size(upload)
return true
end
return true if changing_upload_security?(upload)
if is_authorized?(upload, extension)
if FileHelper.is_supported_image?(upload.original_filename)
authorized_image_extension(upload, extension)
maximum_image_file_size(upload)
else
authorized_attachment_extension(upload, extension)
maximum_attachment_file_size(upload)
end
end
end
# this should only be run on existing records, and covers cases of
# upload.update_secure_status being run outside of the creation flow,
# where some cases e.g. have exemptions on the extension enforcement
def changing_upload_security?(upload)
!upload.new_record? &&
upload.changed_attributes.keys.all? do |attribute|
%w[secure security_last_changed_at security_last_changed_reason].include?(attribute)
end
end
def is_authorized?(upload, extension)
extension_authorized?(upload, extension, authorized_extensions(upload))
end
def authorized_image_extension(upload, extension)
extension_authorized?(upload, extension, authorized_images(upload))
end
def maximum_image_file_size(upload)
maximum_file_size(upload, "image")
end
def authorized_attachment_extension(upload, extension)
extension_authorized?(upload, extension, authorized_attachments(upload))
end
def maximum_attachment_file_size(upload)
maximum_file_size(upload, "attachment")
end
private
def extensions_to_set(exts)
extensions = Set.new
exts
.gsub(/[\s\.]+/, "")
.downcase
.split("|")
.each { |extension| extensions << extension if extension.exclude?("*") }
extensions
end
def authorized_extensions(upload)
extensions =
if upload.for_theme
SiteSetting.theme_authorized_extensions
elsif upload.for_export
SiteSetting.export_authorized_extensions
else
SiteSetting.authorized_extensions
end
extensions_to_set(extensions)
end
def authorized_images(upload)
authorized_extensions(upload) & FileHelper.supported_images
end
def authorized_attachments(upload)
authorized_extensions(upload) - FileHelper.supported_images
end
def authorizes_all_extensions?(upload)
if upload.user&.staff?
return true if SiteSetting.authorized_extensions_for_staff.include?("*")
end
extensions =
if upload.for_theme
SiteSetting.theme_authorized_extensions
elsif upload.for_export
SiteSetting.export_authorized_extensions
else
SiteSetting.authorized_extensions
end
extensions.include?("*")
end
def extension_authorized?(upload, extension, extensions)
return true if authorizes_all_extensions?(upload)
staff_extensions = Set.new
if upload.user&.staff?
staff_extensions = extensions_to_set(SiteSetting.authorized_extensions_for_staff)
return true if staff_extensions.include?(extension.downcase)
end
unless authorized = extensions.include?(extension.downcase)
message =
I18n.t(
"upload.unauthorized",
authorized_extensions: (extensions | staff_extensions).to_a.join(", "),
)
upload.errors.add(:original_filename, message)
end
authorized
end
def maximum_file_size(upload, type)
return if !upload.validate_file_size
max_size_kb =
if upload.for_export
SiteSetting.max_export_file_size_kb
else
if upload.user&.id == Discourse::SYSTEM_USER_ID && type == "attachment"
[
SiteSetting.get("system_user_max_attachment_size_kb"),
SiteSetting.get("max_attachment_size_kb"),
].max
else
SiteSetting.get("max_#{type}_size_kb")
end
end
max_size_bytes = max_size_kb.kilobytes
if upload.filesize > max_size_bytes
message =
I18n.t(
"upload.#{type}s.too_large_humanized",
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes),
)
upload.errors.add(:filesize, message)
end
end
end