# frozen_string_literal: true require "final_destination" require "mini_mime" require "open-uri" class FileHelper def self.log(log_level, message) Rails.logger.public_send( log_level, "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end def self.is_supported_image?(filename) filename.match?(supported_images_regexp) end def self.is_supported_video?(filename) filename.match?(supported_video_regexp) end def self.is_supported_audio?(filename) filename.match?(supported_audio_regexp) end def self.is_inline_image?(filename) filename.match?(inline_images_regexp) end def self.is_supported_media?(filename) filename.match?(supported_media_regexp) end def self.is_supported_playable_media?(filename) filename.match?(supported_playable_media_regexp) end class FakeIO attr_accessor :status end def self.download( url, max_file_size:, tmp_file_name:, follow_redirect: false, read_timeout: 5, skip_rate_limit: false, verbose: false, validate_uri: true, retain_on_max_file_size_exceeded: false, include_port_in_host_header: false ) url = "https:" + url if url.start_with?("//") raise Discourse::InvalidParameters.new(:url) unless url =~ %r{\Ahttps?://} tmp = nil fd = FinalDestination.new( url, max_redirects: follow_redirect ? 5 : 0, skip_rate_limit: skip_rate_limit, verbose: verbose, validate_uri: validate_uri, timeout: read_timeout, include_port_in_host_header: include_port_in_host_header, ) fd.get do |response, chunk, uri| if tmp.nil? # error handling if uri.blank? if response.code.to_i >= 400 # attempt error API compatibility io = FakeIO.new io.status = [response.code, ""] raise OpenURI::HTTPError.new("#{response.code} Error", io) else log(:error, "FinalDestination did not work for: #{url}") if verbose throw :done end end if response.content_type.present? ext = MiniMime.lookup_by_content_type(response.content_type)&.extension ext = "jpg" if ext == "jpe" tmp_file_ext = "." + ext if ext.present? end tmp_file_ext ||= File.extname(uri.path) tmp = Tempfile.new([tmp_file_name, tmp_file_ext]) tmp.binmode end tmp.write(chunk) if tmp.size > max_file_size unless retain_on_max_file_size_exceeded tmp.close tmp = nil end throw :done end end tmp&.rewind tmp end def self.optimize_image!(filename, allow_pngquant: false) image_optim( allow_pngquant: allow_pngquant, strip_image_metadata: SiteSetting.strip_image_metadata, ).optimize_image!(filename) end def self.image_optim(allow_pngquant: false, strip_image_metadata: true) # memoization is critical, initializing an ImageOptim object is very expensive # sometimes up to 200ms searching for binaries and looking at versions memoize("image_optim", allow_pngquant, strip_image_metadata) do pngquant_options = false pngquant_options = { allow_lossy: true } if allow_pngquant ImageOptim.new( # GLOBAL timeout: 15, skip_missing_workers: true, # PNG oxipng: { level: 3, strip: strip_image_metadata, }, optipng: false, advpng: false, pngcrush: false, pngout: false, pngquant: pngquant_options, # JPG jpegoptim: { strip: strip_image_metadata ? "all" : "none", }, jpegtran: false, jpegrecompress: false, # Skip looking for gifsicle, svgo binaries gifsicle: false, svgo: false, ) end end def self.memoize(*args) (@memoized ||= {})[args] ||= yield end def self.supported_gravatar_extensions @@supported_gravatar_images ||= Set.new(%w[jpg jpeg png gif]) end def self.supported_images @@supported_images ||= Set.new %w[jpg jpeg png gif svg ico webp avif] end def self.inline_images # SVG cannot safely be shown as a document @@inline_images ||= supported_images - %w[svg] end def self.supported_audio @@supported_audio ||= Set.new %w[mp3 ogg oga opus wav m4a m4b m4p m4r aac flac] end def self.supported_video @@supported_video ||= Set.new %w[mov mp4 webm ogv m4v 3gp avi mpeg] end def self.supported_video_regexp @@supported_video_regexp ||= /\.(#{supported_video.to_a.join("|")})\z/i end def self.supported_audio_regexp @@supported_audio_regexp ||= /\.(#{supported_audio.to_a.join("|")})\z/i end def self.supported_images_regexp @@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})\z/i end def self.inline_images_regexp @@inline_images_regexp ||= /\.(#{inline_images.to_a.join("|")})\z/i end def self.supported_media_regexp @@supported_media_regexp ||= begin media = supported_images | supported_audio | supported_video /\.(#{media.to_a.join("|")})\z/i end end def self.supported_playable_media_regexp @@supported_playable_media_regexp ||= begin media = supported_audio | supported_video /\.(#{media.to_a.join("|")})\z/i end end end