mirror of
https://github.com/stripe/stripe-ruby.git
synced 2025-05-15 00:01:39 -04:00
132 lines
3.8 KiB
Ruby
132 lines
3.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "securerandom"
|
|
require "tempfile"
|
|
|
|
module Stripe
|
|
# Encodes parameters into a `multipart/form-data` payload as described by RFC
|
|
# 2388:
|
|
#
|
|
# https://tools.ietf.org/html/rfc2388
|
|
#
|
|
# This is most useful for transferring file-like objects.
|
|
#
|
|
# Parameters should be added with `#encode`. When ready, use `#body` to get
|
|
# the encoded result and `#content_type` to get the value that should be
|
|
# placed in the `Content-Type` header of a subsequent request (which includes
|
|
# a boundary value).
|
|
class MultipartEncoder
|
|
MULTIPART_FORM_DATA = "multipart/form-data"
|
|
|
|
# A shortcut for encoding a single set of parameters and finalizing a
|
|
# result.
|
|
#
|
|
# Returns an encoded body and the value that should be set in the content
|
|
# type header of a subsequent request.
|
|
def self.encode(params)
|
|
encoder = MultipartEncoder.new
|
|
encoder.encode(params)
|
|
encoder.close
|
|
[encoder.body, encoder.content_type]
|
|
end
|
|
|
|
# Gets the object's randomly generated boundary string.
|
|
attr_reader :boundary
|
|
|
|
# Initializes a new multipart encoder.
|
|
def initialize
|
|
# Kind of weird, but required by Rubocop because the unary plus operator
|
|
# is considered faster than `Stripe.new`.
|
|
@body = +""
|
|
|
|
# Chose the same number of random bytes that Go uses in its standard
|
|
# library implementation. Easily enough entropy to ensure that it won't
|
|
# be present in a file we're sending.
|
|
@boundary = SecureRandom.hex(30)
|
|
|
|
@closed = false
|
|
@first_field = true
|
|
end
|
|
|
|
# Gets the encoded body. `#close` must be called first.
|
|
def body
|
|
raise "object must be closed before getting body" unless @closed
|
|
|
|
@body
|
|
end
|
|
|
|
# Finalizes the object by writing the final boundary.
|
|
def close
|
|
raise "object already closed" if @closed
|
|
|
|
@body << "\r\n"
|
|
@body << "--#{@boundary}--"
|
|
|
|
@closed = true
|
|
|
|
nil
|
|
end
|
|
|
|
# Gets the value including boundary that should be put into a multipart
|
|
# request's `Content-Type`.
|
|
def content_type
|
|
"#{MULTIPART_FORM_DATA}; boundary=#{@boundary}"
|
|
end
|
|
|
|
# Encodes a set of parameters to the body.
|
|
#
|
|
# Note that parameters are expected to be a hash, but a "flat" hash such
|
|
# that complex substructures like hashes and arrays have already been
|
|
# appropriately Stripe-encoded. Pass a complex structure through
|
|
# `Util.flatten_params` first before handing it off to this method.
|
|
def encode(params)
|
|
raise "no more parameters can be written to closed object" if @closed
|
|
|
|
params.each do |name, val|
|
|
if val.is_a?(::File) || val.is_a?(::Tempfile)
|
|
write_field(name, val.read, filename: ::File.basename(val.path))
|
|
elsif val.respond_to?(:read)
|
|
write_field(name, val.read, filename: "blob")
|
|
else
|
|
write_field(name, val, filename: nil)
|
|
end
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
#
|
|
# private
|
|
#
|
|
|
|
# Escapes double quotes so that the given value can be used in a
|
|
# double-quoted string and replaces any linebreak characters with spaces.
|
|
private def escape(str)
|
|
str.gsub('"', "%22").tr("\n", " ").tr("\r", " ")
|
|
end
|
|
|
|
private def write_field(name, data, filename:)
|
|
if @first_field
|
|
@first_field = false
|
|
else
|
|
@body << "\r\n"
|
|
end
|
|
|
|
@body << "--#{@boundary}\r\n"
|
|
|
|
if filename
|
|
@body << (%(Content-Disposition: form-data) +
|
|
%(; name="#{escape(name.to_s)}") +
|
|
%(; filename="#{escape(filename)}"\r\n))
|
|
@body << %(Content-Type: application/octet-stream\r\n)
|
|
else
|
|
@body << (%(Content-Disposition: form-data) +
|
|
%(; name="#{escape(name.to_s)}"\r\n))
|
|
end
|
|
|
|
@body << "\r\n"
|
|
@body << data.to_s
|
|
end
|
|
end
|
|
end
|