cloudinary 1.20.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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