summaryrefslogtreecommitdiff
path: root/lib/uploaded_file.rb
blob: 9b034d1c6c2f637180a14f9757d71e7acb14f7ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# frozen_string_literal: true

require "tempfile"
require "tmpdir"
require "fileutils"

class UploadedFile
  InvalidPathError = Class.new(StandardError)
  UnknownSizeError = Class.new(StandardError)

  # The filename, *not* including the path, of the "uploaded" file
  attr_reader :original_filename

  # The tempfile
  attr_reader :tempfile

  # The content type of the "uploaded" file
  attr_accessor :content_type

  attr_reader :remote_id
  attr_reader :sha256
  attr_reader :size

  def initialize(path, filename: nil, content_type: "application/octet-stream", sha256: nil, remote_id: nil, size: nil)
    if path.present?
      raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path)

      @tempfile = File.new(path, 'rb')
      @size = @tempfile.size
    else
      begin
        @size = Integer(size)
      rescue ArgumentError, TypeError
        raise UnknownSizeError, 'Unable to determine file size'
      end
    end

    @content_type = content_type
    @original_filename = sanitize_filename(filename || path || '')
    @content_type = content_type
    @sha256 = sha256
    @remote_id = remote_id
  end

  # TODO this function is meant to replace .from_params when the feature flag
  # upload_middleware_jwt_params_handler is removed
  # See https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
  def self.from_params_without_field(params, upload_paths)
    path = params['path']
    remote_id = params['remote_id']
    return if path.blank? && remote_id.blank?

    # don't use file_path if remote_id is set
    if remote_id.present?
      file_path = nil
    elsif path.present?
      file_path = File.realpath(path)

      unless self.allowed_path?(file_path, Array(upload_paths).compact)
        raise InvalidPathError, "insecure path used '#{file_path}'"
      end
    end

    UploadedFile.new(
      file_path,
      filename: params['name'],
      content_type: params['type'] || 'application/octet-stream',
      sha256: params['sha256'],
      remote_id: remote_id,
      size: params['size']
    )
  end

  # Deprecated. Don't use it.
  # .from_params_without_field will replace this one
  # See .from_params_without_field and
  # https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
  def self.from_params(params, field, upload_paths, path_override = nil)
    path = path_override || params["#{field}.path"]
    remote_id = params["#{field}.remote_id"]
    return if path.blank? && remote_id.blank?

    if remote_id.present? # don't use file_path if remote_id is set
      file_path = nil
    elsif path.present?
      file_path = File.realpath(path)

      unless self.allowed_path?(file_path, Array(upload_paths).compact)
        raise InvalidPathError, "insecure path used '#{file_path}'"
      end
    end

    UploadedFile.new(file_path,
      filename: params["#{field}.name"],
      content_type: params["#{field}.type"] || 'application/octet-stream',
      sha256: params["#{field}.sha256"],
      remote_id: remote_id,
      size: params["#{field}.size"])
  end

  def self.allowed_path?(file_path, paths)
    paths.any? do |path|
      File.exist?(path) && file_path.start_with?(File.realpath(path))
    end
  end

  # copy-pasted from CarrierWave::SanitizedFile
  def sanitize_filename(name)
    name = name.tr("\\", "/") # work-around for IE
    name = ::File.basename(name)
    name = name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, "_")
    name = "_#{name}" if name =~ /\A\.+\z/
    name = "unnamed" if name.empty?
    name.mb_chars.to_s
  end

  def path
    @tempfile&.path
  end

  def close
    @tempfile&.close
  end

  alias_method :local_path, :path

  def method_missing(method_name, *args, &block) #:nodoc:
    @tempfile.__send__(method_name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
  end

  def respond_to?(method_name, include_private = false) #:nodoc:
    @tempfile.respond_to?(method_name, include_private) || super
  end
end