cloudinary 1.20.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,7 +21,12 @@ module Cloudinary
21
21
  start = options[:start_time]
22
22
  expiration = options[:expiration]
23
23
  ip = options[:ip]
24
+
24
25
  acl = options[:acl]
26
+ if acl.present?
27
+ acl = acl.is_a?(String) ? [acl] : acl
28
+ end
29
+
25
30
  duration = options[:duration]
26
31
  url = options[:url]
27
32
  start = Time.new.getgm.to_i if start == 'now'
@@ -41,9 +46,9 @@ module Cloudinary
41
46
  token << "ip=#{ip}" if ip
42
47
  token << "st=#{start}" if start
43
48
  token << "exp=#{expiration}"
44
- token << "acl=#{escape_to_lower(acl)}" if acl
49
+ token << "acl=#{escape_to_lower(acl.join('!'))}" if acl && acl.size > 0
45
50
  to_sign = token.clone
46
- to_sign << "url=#{escape_to_lower(url)}" if url
51
+ to_sign << "url=#{escape_to_lower(url)}" if url && (acl.blank? || acl.size == 0)
47
52
  auth = digest(to_sign.join(SEPARATOR), key)
48
53
  token << "hmac=#{auth}"
49
54
  "#{name}=#{token.join(SEPARATOR)}"
@@ -19,9 +19,12 @@ module Cloudinary::BaseApi
19
19
  # This sets the instantiated self as the response Hash
20
20
  update Cloudinary::Api.parse_json_response response
21
21
 
22
- @rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i if response.headers[:x_featureratelimit_limit]
23
- @rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset]) if response.headers[:x_featureratelimit_reset]
24
- @rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i if response.headers[:x_featureratelimit_remaining]
22
+ # According to RFC 2616, header names are case-insensitive.
23
+ lc_headers = response.headers.transform_keys(&:downcase)
24
+
25
+ @rate_limit_allowed = lc_headers[:x_featureratelimit_limit].to_i if lc_headers[:x_featureratelimit_limit]
26
+ @rate_limit_reset_at = Time.parse(lc_headers[:x_featureratelimit_reset]) if lc_headers[:x_featureratelimit_reset]
27
+ @rate_limit_remaining = lc_headers[:x_featureratelimit_remaining].to_i if lc_headers[:x_featureratelimit_remaining]
25
28
  end
26
29
  end
27
30
  end
@@ -59,13 +62,14 @@ module Cloudinary::BaseApi
59
62
 
60
63
  private
61
64
 
62
- def call_cloudinary_api(method, uri, user, password, params, options, &api_url_builder)
65
+ def call_cloudinary_api(method, uri, auth, params, options, &api_url_builder)
63
66
  cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || 'https://api.cloudinary.com'
64
67
  api_url = Cloudinary::Utils.smart_escape(api_url_builder.call(cloudinary, uri).flatten.join('/'))
65
68
  timeout = options[:timeout] || Cloudinary.config.timeout || 60
66
69
  proxy = options[:api_proxy] || Cloudinary.config.api_proxy
67
70
 
68
71
  headers = { "User-Agent" => Cloudinary::USER_AGENT }
72
+ headers.merge!("Authorization" => get_authorization_header_value(auth))
69
73
 
70
74
  if options[:content_type] == :json
71
75
  payload = params.to_json
@@ -74,6 +78,21 @@ module Cloudinary::BaseApi
74
78
  payload = params.reject { |_, v| v.nil? || v == "" }
75
79
  end
76
80
 
77
- call_json_api(method, api_url, payload, timeout, headers, proxy, user, password)
81
+ call_json_api(method, api_url, payload, timeout, headers, proxy)
82
+ end
83
+
84
+ def get_authorization_header_value(auth)
85
+ if auth[:oauth_token].present?
86
+ "Bearer #{auth[:oauth_token]}"
87
+ else
88
+ "Basic #{Base64.urlsafe_encode64("#{auth[:key]}:#{auth[:secret]}")}"
89
+ end
90
+ end
91
+
92
+ def validate_authorization(api_key, api_secret, oauth_token)
93
+ return if oauth_token.present?
94
+
95
+ raise("Must supply api_key") if api_key.nil?
96
+ raise("Must supply api_secret") if api_secret.nil?
78
97
  end
79
98
  end
@@ -154,8 +154,16 @@ module Cloudinary::CarrierWave
154
154
  format = Cloudinary::PreloadedFile.split_format(original_filename || "").last
155
155
  return format || "" if resource_type == "raw"
156
156
  format = requested_format || format || default_format
157
-
157
+
158
158
  format = format.to_s.downcase
159
159
  Cloudinary::FORMAT_ALIASES[format] || format
160
160
  end
161
+
162
+ def store!(new_file=nil)
163
+ super
164
+
165
+ column = model.send(:_mounter, mounted_as).send(:serialization_column)
166
+ identifier = model.send(:attribute, column)
167
+ retrieve_from_store!(identifier) unless identifier.nil?
168
+ end
161
169
  end
@@ -1,9 +1,14 @@
1
1
  class Cloudinary::Search
2
+ SORT_BY = :sort_by
3
+ AGGREGATE = :aggregate
4
+ WITH_FIELD = :with_field
5
+ KEYS_WITH_UNIQUE_VALUES = [SORT_BY, AGGREGATE, WITH_FIELD].freeze
6
+
2
7
  def initialize
3
8
  @query_hash = {
4
- :sort_by => [],
5
- :aggregate => [],
6
- :with_field => []
9
+ SORT_BY => {},
10
+ AGGREGATE => {},
11
+ WITH_FIELD => {}
7
12
  }
8
13
  end
9
14
 
@@ -28,23 +33,51 @@ class Cloudinary::Search
28
33
  self
29
34
  end
30
35
 
36
+ # Sets the `sort_by` field.
37
+ #
38
+ # @param [String] field_name The field to sort by. You can specify more than one sort_by parameter;
39
+ # results will be sorted according to the order of the fields provided.
40
+ # @param [String] dir Sort direction. Valid sort directions are 'asc' or 'desc'. Default: 'desc'.
41
+ #
42
+ # @return [Cloudinary::Search]
31
43
  def sort_by(field_name, dir = 'desc')
32
- @query_hash[:sort_by].push(field_name => dir)
44
+ @query_hash[SORT_BY][field_name] = { field_name => dir }
33
45
  self
34
46
  end
35
47
 
48
+ # The name of a field (attribute) for which an aggregation count should be calculated and returned in the response.
49
+ #
50
+ # You can specify more than one aggregate parameter.
51
+ #
52
+ # @param [String] value Supported values: resource_type, type, pixels (only the image assets in the response are
53
+ # aggregated), duration (only the video assets in the response are aggregated), format, and
54
+ # bytes. For aggregation fields without discrete values, the results are divided into
55
+ # categories.
56
+ # @return [Cloudinary::Search]
36
57
  def aggregate(value)
37
- @query_hash[:aggregate].push(value)
58
+ @query_hash[AGGREGATE][value] = value
38
59
  self
39
60
  end
40
61
 
62
+ # The name of an additional asset attribute to include for each asset in the response.
63
+ #
64
+ # @param [String] value Possible value: context, tags, and for Tier 2 also image_metadata, and image_analysis.
65
+ #
66
+ # @return [Cloudinary::Search]
41
67
  def with_field(value)
42
- @query_hash[:with_field].push(value)
68
+ @query_hash[WITH_FIELD][value] = value
43
69
  self
44
70
  end
45
71
 
72
+ # Returns the query as an hash.
73
+ #
74
+ # @return [Hash]
46
75
  def to_h
47
- @query_hash.select { |_, value| !value.nil? && !(value.is_a?(Array) && value.empty?) }
76
+ @query_hash.each_with_object({}) do |(key, value), query|
77
+ next if value.nil? || ((value.is_a?(Array) || value.is_a?(Hash)) && value.blank?)
78
+
79
+ query[key] = KEYS_WITH_UNIQUE_VALUES.include?(key) ? value.values : value
80
+ end
48
81
  end
49
82
 
50
83
  def execute(options = {})
@@ -21,6 +21,7 @@ class Cloudinary::Uploader
21
21
  :access_control => Cloudinary::Utils.json_array_param(options[:access_control]),
22
22
  :access_mode => options[:access_mode],
23
23
  :allowed_formats => Cloudinary::Utils.build_array(options[:allowed_formats]).join(","),
24
+ :asset_folder => options[:asset_folder],
24
25
  :async => Cloudinary::Utils.as_safe_bool(options[:async]),
25
26
  :auto_tagging => options[:auto_tagging] && options[:auto_tagging].to_f,
26
27
  :background_removal => options[:background_removal],
@@ -33,6 +34,7 @@ class Cloudinary::Uploader
33
34
  :custom_coordinates => Cloudinary::Utils.encode_double_array(options[:custom_coordinates]),
34
35
  :detection => options[:detection],
35
36
  :discard_original_filename => Cloudinary::Utils.as_safe_bool(options[:discard_original_filename]),
37
+ :display_name => options[:display_name],
36
38
  :eager => Cloudinary::Utils.build_eager(options[:eager]),
37
39
  :eager_async => Cloudinary::Utils.as_safe_bool(options[:eager_async]),
38
40
  :eager_notification_url => options[:eager_notification_url],
@@ -53,6 +55,7 @@ class Cloudinary::Uploader
53
55
  :phash => Cloudinary::Utils.as_safe_bool(options[:phash]),
54
56
  :proxy => options[:proxy],
55
57
  :public_id => options[:public_id],
58
+ :public_id_prefix => options[:public_id_prefix],
56
59
  :quality_analysis => Cloudinary::Utils.as_safe_bool(options[:quality_analysis]),
57
60
  :quality_override => options[:quality_override],
58
61
  :raw_convert => options[:raw_convert],
@@ -66,6 +69,7 @@ class Cloudinary::Uploader
66
69
  :unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
67
70
  :upload_preset => options[:upload_preset],
68
71
  :use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
72
+ :use_filename_as_display_name => Cloudinary::Utils.as_safe_bool(options[:use_filename_as_display_name]),
69
73
  :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis]),
70
74
  :metadata => Cloudinary::Utils.encode_context(options[:metadata])
71
75
  }
@@ -110,6 +114,9 @@ class Cloudinary::Uploader
110
114
  else
111
115
  filename = "cloudinaryfile"
112
116
  end
117
+
118
+ filename = options[:filename] if options[:filename]
119
+
113
120
  unique_upload_id = Cloudinary::Utils.random_public_id
114
121
  upload = nil
115
122
  index = 0
@@ -159,7 +166,9 @@ class Cloudinary::Uploader
159
166
  :from_public_id => from_public_id,
160
167
  :to_public_id => to_public_id,
161
168
  :to_type => options[:to_type],
162
- :invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate])
169
+ :invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate]),
170
+ :context => options[:context],
171
+ :metadata => options[:metadata]
163
172
  }
164
173
  end
165
174
  end
@@ -207,17 +216,42 @@ class Cloudinary::Uploader
207
216
  end
208
217
  end
209
218
 
210
- def self.generate_sprite(tag, options={})
219
+ SLIDESHOW_PARAMS = [:notification_url, :public_id, :upload_preset]
220
+
221
+ # Creates auto-generated video slideshow.
222
+ #
223
+ # @param [Hash] options Additional options.
224
+ #
225
+ # @return [Hash] Hash with meta information URLs of generated slideshow resources.
226
+ def self.create_slideshow(options = {})
227
+ options[:resource_type] ||= :video
228
+
229
+ call_api("create_slideshow", options) do
230
+ params = {
231
+ :timestamp => Time.now.to_i,
232
+ :transformation => Cloudinary::Utils.build_eager(options[:transformation]),
233
+ :manifest_transformation => Cloudinary::Utils.build_eager(options[:manifest_transformation]),
234
+ :manifest_json => options[:manifest_json] && options[:manifest_json].to_json,
235
+ :tags => options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
236
+ :overwrite => Cloudinary::Utils.as_safe_bool(options[:overwrite])
237
+ }
238
+ SLIDESHOW_PARAMS.each { |k| params[k] = options[k] unless options[k].nil? }
239
+
240
+ params
241
+ end
242
+ end
243
+
244
+ # Generates sprites by merging multiple images into a single large image.
245
+ #
246
+ # @param [String|Hash] tag Treated as additional options when hash is passed, otherwise as a tag
247
+ # @param [Hash] options Additional options. Should be omitted when +tag_or_options+ is a Hash
248
+ #
249
+ # @return [Hash] Hash with meta information URLs of generated sprite resources
250
+ def self.generate_sprite(tag, options = {})
211
251
  version_store = options.delete(:version_store)
212
252
 
213
253
  result = call_api("sprite", options) do
214
- {
215
- :timestamp => (options[:timestamp] || Time.now.to_i),
216
- :tag => tag,
217
- :async => options[:async],
218
- :notification_url => options[:notification_url],
219
- :transformation => Cloudinary::Utils.generate_transformation_string(options.merge(:fetch_format => options[:format]))
220
- }
254
+ Cloudinary::Utils.build_multi_and_sprite_params(tag, options)
221
255
  end
222
256
 
223
257
  if version_store == :file && result && result["version"]
@@ -229,16 +263,15 @@ class Cloudinary::Uploader
229
263
  return result
230
264
  end
231
265
 
232
- def self.multi(tag, options={})
266
+ # Creates either a single animated image, video or a PDF.
267
+ #
268
+ # @param [String|Hash] tag Treated as additional options when hash is passed, otherwise as a tag
269
+ # @param [Hash] options Additional options. Should be omitted when +tag_or_options+ is a Hash
270
+ #
271
+ # @return [Hash] Hash with meta information URLs of the generated file
272
+ def self.multi(tag, options = {})
233
273
  call_api("multi", options) do
234
- {
235
- :timestamp => (options[:timestamp] || Time.now.to_i),
236
- :tag => tag,
237
- :format => options[:format],
238
- :async => options[:async],
239
- :notification_url => options[:notification_url],
240
- :transformation => Cloudinary::Utils.generate_transformation_string(options.clone)
241
- }
274
+ Cloudinary::Utils.build_multi_and_sprite_params(tag, options)
242
275
  end
243
276
  end
244
277
 
@@ -337,11 +370,19 @@ class Cloudinary::Uploader
337
370
  options = options.clone
338
371
  return_error = options.delete(:return_error)
339
372
  use_cache = options[:use_cache] || Cloudinary.config.use_cache
340
-
341
373
  params, non_signable = yield
342
374
  non_signable ||= []
343
375
 
344
- unless options[:unsigned]
376
+ headers = { "User-Agent" => Cloudinary::USER_AGENT }
377
+ headers['Content-Range'] = options[:content_range] if options[:content_range]
378
+ headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
379
+ headers.merge!(options[:extra_headers]) if options[:extra_headers]
380
+
381
+ oauth_token = options[:oauth_token] || Cloudinary.config.oauth_token
382
+
383
+ if oauth_token
384
+ headers["Authorization"] = "Bearer #{oauth_token}"
385
+ elsif !options[:unsigned]
345
386
  api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
346
387
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
347
388
  signature_algorithm = options[:signature_algorithm]
@@ -353,11 +394,7 @@ class Cloudinary::Uploader
353
394
 
354
395
  result = nil
355
396
 
356
- api_url = Cloudinary::Utils.cloudinary_api_url(action, options)
357
- headers = { "User-Agent" => Cloudinary::USER_AGENT }
358
- headers['Content-Range'] = options[:content_range] if options[:content_range]
359
- headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
360
- headers.merge!(options[:extra_headers]) if options[:extra_headers]
397
+ api_url = Cloudinary::Utils.cloudinary_api_url(action, options)
361
398
  RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers, :proxy => proxy) do
362
399
  |response, request, tmpresult|
363
400
  raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" unless [200, 400, 401, 403, 404, 500].include?(response.code)
@@ -13,6 +13,7 @@ require 'cloudinary/responsive'
13
13
  class Cloudinary::Utils
14
14
  # @deprecated Use Cloudinary::SHARED_CDN
15
15
  SHARED_CDN = Cloudinary::SHARED_CDN
16
+ MODE_DOWNLOAD = "download"
16
17
  DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
17
18
  CONDITIONAL_OPERATORS = {
18
19
  "=" => 'eq',
@@ -334,7 +335,7 @@ class Cloudinary::Utils
334
335
  "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
335
336
  end
336
337
 
337
- EXP_REGEXP = Regexp.new('(\$_*[^_ ]+)|(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
338
+ EXP_REGEXP = Regexp.new('(\$_*[^_ ]+)|(?<![\$:])('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
338
339
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
339
340
 
340
341
  def self.normalize_expression(expression)
@@ -351,18 +352,30 @@ class Cloudinary::Utils
351
352
  # @return [string] layer transformation string
352
353
  # @private
353
354
  def self.process_layer(layer)
355
+ if layer.is_a? String and layer.start_with?("fetch:")
356
+ layer = {:url => layer[6..-1]} # omit "fetch:" prefix
357
+ end
354
358
  if layer.is_a? Hash
355
359
  layer = symbolize_keys layer
356
360
  public_id = layer[:public_id]
357
361
  format = layer[:format]
362
+ fetch = layer[:url]
358
363
  resource_type = layer[:resource_type] || "image"
359
- type = layer[:type] || "upload"
364
+ type = layer[:type]
360
365
  text = layer[:text]
361
366
  text_style = nil
362
367
  components = []
363
368
 
369
+ if type.nil?
370
+ if fetch.nil?
371
+ type = "upload"
372
+ else
373
+ type = "fetch"
374
+ end
375
+ end
376
+
364
377
  if public_id.present?
365
- if type == "fetch" && public_id.match(%r(^https?:/)i)
378
+ if type == "fetch" and public_id.match(%r(^https?:/)i)
366
379
  public_id = Base64.urlsafe_encode64(public_id)
367
380
  else
368
381
  public_id = public_id.gsub("/", ":")
@@ -370,14 +383,15 @@ class Cloudinary::Utils
370
383
  end
371
384
  end
372
385
 
373
- if text.blank? && resource_type != "text"
374
- if public_id.blank?
386
+ if fetch.present? && fetch.match(%r(^https?:/)i)
387
+ fetch = Base64.urlsafe_encode64(fetch)
388
+ elsif text.blank? && resource_type != "text"
389
+ if public_id.blank? && type != "fetch"
375
390
  raise(CloudinaryException, "Must supply public_id for resource_type layer_parameter")
376
391
  end
377
392
  if resource_type == "subtitles"
378
393
  text_style = text_style(layer)
379
394
  end
380
-
381
395
  else
382
396
  resource_type = "text"
383
397
  type = nil
@@ -403,6 +417,7 @@ class Cloudinary::Utils
403
417
  components.push(type) if type != "upload"
404
418
  components.push(text_style)
405
419
  components.push(public_id)
420
+ components.push(fetch)
406
421
  components.push(text)
407
422
  layer = components.reject(&:blank?).join(":")
408
423
  end
@@ -430,6 +445,8 @@ class Cloudinary::Utils
430
445
  ]
431
446
 
432
447
  def self.text_style(layer)
448
+ return layer[:text_style] if layer[:text_style].present?
449
+
433
450
  font_family = layer[:font_family]
434
451
  font_size = layer[:font_size]
435
452
  keywords = []
@@ -719,6 +736,43 @@ class Cloudinary::Utils
719
736
  params
720
737
  end
721
738
 
739
+ # Helper method for generating download URLs
740
+ #
741
+ # @param [String] action @see Cloudinary::Utils.cloudinary_api_url
742
+ # @param [Hash] params Query parameters in generated URL
743
+ # @param [Hash] options Additional options
744
+ # @yield [query_parameters] Invokes the block with query parameters to override how to encode them
745
+ #
746
+ # @return [String]
747
+ def self.cloudinary_api_download_url(action, params, options = {})
748
+ cloudinary_params = sign_request(params.merge(mode: MODE_DOWNLOAD), options)
749
+
750
+ "#{Cloudinary::Utils.cloudinary_api_url(action, options)}?#{hash_query_params(cloudinary_params)}"
751
+ end
752
+ private_class_method :cloudinary_api_download_url
753
+
754
+ # Return a signed URL to the 'generate_sprite' endpoint with 'mode=download'.
755
+ #
756
+ # @param [String|Hash] tag Treated as additional options when hash is passed, otherwise as a tag
757
+ # @param [Hash] options Additional options. Should be omitted when +tag_or_options+ is a Hash
758
+ #
759
+ # @return [String] The signed URL to download sprite
760
+ def self.download_generated_sprite(tag, options = {})
761
+ params = build_multi_and_sprite_params(tag, options)
762
+ cloudinary_api_download_url("sprite", params, options)
763
+ end
764
+
765
+ # Return a signed URL to the 'multi' endpoint with 'mode=download'.
766
+ #
767
+ # @param [String|Hash] tag Treated as additional options when hash is passed, otherwise as a tag
768
+ # @param [Hash] options Additional options. Should be omitted when +tag_or_options+ is a Hash
769
+ #
770
+ # @return [String] The signed URL to download multi
771
+ def self.download_multi(tag, options = {})
772
+ params = build_multi_and_sprite_params(tag, options)
773
+ cloudinary_api_download_url("multi", params, options)
774
+ end
775
+
722
776
  def self.private_download_url(public_id, format, options = {})
723
777
  cloudinary_params = sign_request({
724
778
  :timestamp=>Time.now.to_i,
@@ -767,11 +821,10 @@ class Cloudinary::Utils
767
821
  # @option options [String] :keep_derived (false) keep the derived images used for generating the archive
768
822
  # @return [String] archive url
769
823
  def self.download_archive_url(options = {})
770
- cloudinary_params = sign_request(Cloudinary::Utils.archive_params(options.merge(:mode => "download")), options)
771
- return Cloudinary::Utils.cloudinary_api_url("generate_archive", options) + "?" + hash_query_params(cloudinary_params)
824
+ params = Cloudinary::Utils.archive_params(options)
825
+ cloudinary_api_download_url("generate_archive", params, options)
772
826
  end
773
827
 
774
-
775
828
  # Returns a URL that when invokes creates an zip archive and returns it.
776
829
  # @see download_archive_url
777
830
  def self.download_zip_url(options = {})
@@ -832,7 +885,7 @@ class Cloudinary::Utils
832
885
 
833
886
  # Based on CGI::unescape. In addition keeps '+' character as is
834
887
  def self.smart_unescape(string)
835
- CGI.unescape(string.sub('+', '%2B'))
888
+ CGI.unescape(string.gsub('+', '%2B'))
836
889
  end
837
890
 
838
891
  def self.random_public_id
@@ -1050,7 +1103,7 @@ class Cloudinary::Utils
1050
1103
  Cloudinary::AuthToken.generate options
1051
1104
 
1052
1105
  end
1053
-
1106
+
1054
1107
  private
1055
1108
 
1056
1109
 
@@ -1067,24 +1120,24 @@ class Cloudinary::Utils
1067
1120
  source
1068
1121
  end
1069
1122
  private_class_method :fully_unescape
1070
-
1123
+
1071
1124
  def self.hash_query_params(hash)
1072
1125
  if hash.respond_to?("to_query")
1073
1126
  hash.to_query
1074
1127
  else
1075
- flat_hash_to_query_params(hash)
1128
+ flat_hash_to_query_params(hash)
1076
1129
  end
1077
1130
  end
1078
1131
 
1079
1132
  def self.flat_hash_to_query_params(hash)
1080
- hash.collect do |key, value|
1133
+ hash.collect do |key, value|
1081
1134
  if value.is_a?(Array)
1082
1135
  value.map{|v| "#{CGI.escape(key.to_s)}[]=#{CGI.escape(v.to_s)}"}.join("&")
1083
- else
1136
+ else
1084
1137
  "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
1085
- end
1138
+ end
1086
1139
  end.compact.sort!.join('&')
1087
- end
1140
+ end
1088
1141
 
1089
1142
  def self.number_pattern
1090
1143
  "([0-9]*)\\.([0-9]+)|([0-9]+)"
@@ -1132,9 +1185,10 @@ class Cloudinary::Utils
1132
1185
 
1133
1186
  # A video codec parameter can be either a String or a Hash.
1134
1187
  #
1135
- # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
1188
+ # @param [Object] param <code>vc_<codec>[ : <profile> : [<level> : [<b_frames>]]]</code>
1136
1189
  # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
1137
- # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
1190
+ # or <code>{ codec: 'h265', profile: 'auto', level: 'auto', b_frames: false }</code>
1191
+ # @return [String] <code><codec> : <profile> : [<level> : [<b_frames>]]]</code> if a Hash was provided
1138
1192
  # or the param if a String was provided.
1139
1193
  # Returns NIL if param is not a Hash or String
1140
1194
  # @private
@@ -1148,6 +1202,9 @@ class Cloudinary::Utils
1148
1202
  video.concat ":" + param[:profile]
1149
1203
  if param.has_key? :level
1150
1204
  video.concat ":" + param[:level]
1205
+ if param.has_key?(:b_frames) && param[:b_frames] === false
1206
+ video.concat ":bframes_no"
1207
+ end
1151
1208
  end
1152
1209
  end
1153
1210
  end
@@ -1191,6 +1248,41 @@ class Cloudinary::Utils
1191
1248
  REMOTE_URL_REGEX === url
1192
1249
  end
1193
1250
 
1251
+ # Build params for multi, download_multi, generate_sprite, and download_generated_sprite methods
1252
+ #
1253
+ # @param [String|Hash] tag_or_options Treated as additional options when hash is passed, otherwise as a tag
1254
+ # @param [Hash] options Additional options. Should be omitted when +tag_or_options+ is a Hash
1255
+ #
1256
+ # @return [Hash]
1257
+ #
1258
+ # @private
1259
+ def self.build_multi_and_sprite_params(tag_or_options, options)
1260
+ if tag_or_options.is_a?(Hash)
1261
+ if options.blank?
1262
+ options = tag_or_options
1263
+ tag_or_options = nil
1264
+ else
1265
+ raise "First argument must be a tag when additional options are passed"
1266
+ end
1267
+ end
1268
+ urls = options.delete(:urls)
1269
+
1270
+ if tag_or_options.blank? && urls.blank?
1271
+ raise "Either tag or urls are required"
1272
+ end
1273
+
1274
+ {
1275
+ :tag => tag_or_options,
1276
+ :urls => urls,
1277
+ :transformation => Cloudinary::Utils.generate_transformation_string(options.merge(:fetch_format => options[:format])),
1278
+ :notification_url => options[:notification_url],
1279
+ :format => options[:format],
1280
+ :async => options[:async],
1281
+ :mode => options[:mode],
1282
+ :timestamp => (options[:timestamp] || Time.now.to_i)
1283
+ }
1284
+ end
1285
+
1194
1286
  # The returned url should allow downloading the backedup asset based on the version and asset id
1195
1287
  #
1196
1288
  # asset and version id are returned with resource(<PUBLIC_ID1>, { versions: true })
@@ -1224,10 +1316,53 @@ class Cloudinary::Utils
1224
1316
  end
1225
1317
  end
1226
1318
 
1319
+ # Verifies the authenticity of an API response signature.
1320
+ #
1321
+ # @param [String] public_id he public id of the asset as returned in the API response
1322
+ # @param [Fixnum] version The version of the asset as returned in the API response
1323
+ # @param [String] signature Actual signature. Can be retrieved from the X-Cld-Signature header
1324
+ # @param [Symbol|nil] signature_algorithm Algorithm to use for computing hash
1325
+ # @param [Hash] options
1326
+ # @option options [String] :api_secret API secret, if not passed taken from global config
1327
+ #
1328
+ # @return [Boolean]
1329
+ def self.verify_api_response_signature(public_id, version, signature, signature_algorithm = nil, options = {})
1330
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
1331
+
1332
+ parameters_to_sign = {
1333
+ :public_id => public_id,
1334
+ :version => version
1335
+ }
1336
+
1337
+ signature == api_sign_request(parameters_to_sign, api_secret, signature_algorithm)
1338
+ end
1339
+
1340
+ # Verifies the authenticity of a notification signature.
1341
+ #
1342
+ # @param [String] body JSON of the request's body
1343
+ # @param [Fixnum] timestamp Unix timestamp. Can be retrieved from the X-Cld-Timestamp header
1344
+ # @param [String] signature Actual signature. Can be retrieved from the X-Cld-Signature header
1345
+ # @param [Fixnum] valid_for The desired time in seconds for considering the request valid
1346
+ # @param [Symbol|nil] signature_algorithm Algorithm to use for computing hash
1347
+ # @param [Hash] options
1348
+ # @option options [String] :api_secret API secret, if not passed taken from global config
1349
+ #
1350
+ # @return [Boolean]
1351
+ def self.verify_notification_signature(body, timestamp, signature, valid_for = 7200, signature_algorithm = nil, options = {})
1352
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
1353
+ raise("Body should be of String type") unless body.is_a?(String)
1354
+ # verify that signature is valid for the given timestamp
1355
+ return false if timestamp < (Time.now - valid_for).to_i
1356
+
1357
+ payload_hash = hash("#{body}#{timestamp}#{api_secret}", signature_algorithm, :hexdigest)
1358
+
1359
+ signature == payload_hash
1360
+ end
1361
+
1227
1362
  # Computes hash from input string using specified algorithm.
1228
1363
  #
1229
1364
  # @param [String] input String which to compute hash from
1230
- # @param [String|nil] signature_algorithm Algorithm to use for computing hash
1365
+ # @param [Symbol|nil] signature_algorithm Algorithm to use for computing hash
1231
1366
  # @param [Symbol] hash_method Hash method applied to a signature algorithm (:digest or :hexdigest)
1232
1367
  #
1233
1368
  # @return [String] Computed hash value
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.20.0"
3
+ VERSION = "1.23.0"
4
4
  end