cloudinary 1.11.1 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  4. data/.github/pull_request_template.md +24 -0
  5. data/.gitignore +6 -1
  6. data/.travis.yml +15 -5
  7. data/CHANGELOG.md +149 -0
  8. data/Rakefile +3 -45
  9. data/cloudinary.gemspec +20 -4
  10. data/lib/active_storage/blob_key.rb +20 -0
  11. data/lib/active_storage/service/cloudinary_service.rb +229 -0
  12. data/lib/cloudinary.rb +31 -22
  13. data/lib/cloudinary/api.rb +173 -5
  14. data/lib/cloudinary/auth_token.rb +6 -4
  15. data/lib/cloudinary/carrier_wave.rb +3 -1
  16. data/lib/cloudinary/carrier_wave/remote.rb +3 -2
  17. data/lib/cloudinary/carrier_wave/storage.rb +2 -1
  18. data/lib/cloudinary/cloudinary_controller.rb +2 -4
  19. data/lib/cloudinary/helper.rb +30 -3
  20. data/lib/cloudinary/railtie.rb +7 -3
  21. data/lib/cloudinary/uploader.rb +35 -6
  22. data/lib/cloudinary/utils.rb +112 -40
  23. data/lib/cloudinary/version.rb +1 -1
  24. data/lib/cloudinary/video_helper.rb +96 -22
  25. data/lib/tasks/cloudinary/fetch_assets.rake +48 -0
  26. data/lib/tasks/{cloudinary.rake → cloudinary/sync_static.rake} +0 -0
  27. data/tools/allocate_test_cloud.sh +9 -0
  28. data/tools/get_test_cloud.sh +9 -0
  29. data/tools/update_version +29 -11
  30. data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +48 -14
  31. data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +24 -4
  32. data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +741 -561
  33. data/vendor/assets/javascripts/cloudinary/load-image.all.min.js +1 -1
  34. metadata +62 -67
  35. data/spec/access_control_spec.rb +0 -102
  36. data/spec/api_spec.rb +0 -567
  37. data/spec/archive_spec.rb +0 -129
  38. data/spec/auth_token_spec.rb +0 -77
  39. data/spec/cache_spec.rb +0 -109
  40. data/spec/cloudinary_helper_spec.rb +0 -325
  41. data/spec/cloudinary_spec.rb +0 -32
  42. data/spec/data/sync_static/app/assets/javascripts/1.coffee +0 -1
  43. data/spec/data/sync_static/app/assets/javascripts/1.js +0 -1
  44. data/spec/data/sync_static/app/assets/stylesheets/1.css +0 -3
  45. data/spec/docx.docx +0 -0
  46. data/spec/favicon.ico +0 -0
  47. data/spec/image_spec.rb +0 -107
  48. data/spec/logo.png +0 -0
  49. data/spec/rake_spec.rb +0 -160
  50. data/spec/sample_asset_file.tsv +0 -4
  51. data/spec/search_spec.rb +0 -109
  52. data/spec/spec_helper.rb +0 -266
  53. data/spec/storage_spec.rb +0 -44
  54. data/spec/streaminig_profiles_api_spec.rb +0 -74
  55. data/spec/support/helpers/temp_file_helpers.rb +0 -22
  56. data/spec/support/shared_contexts/rake.rb +0 -19
  57. data/spec/uploader_spec.rb +0 -392
  58. data/spec/utils_methods_spec.rb +0 -54
  59. data/spec/utils_spec.rb +0 -970
  60. data/spec/video_tag_spec.rb +0 -253
  61. data/spec/video_url_spec.rb +0 -185
@@ -1,8 +1,12 @@
1
1
  class Cloudinary::Railtie < Rails::Railtie
2
2
  rake_tasks do
3
- Dir[File.join(File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f }
4
- end
3
+ Dir[File.join(File.dirname(__FILE__),'../tasks/**/*.rake')].each { |f| load f }
4
+ end
5
5
  config.after_initialize do |app|
6
6
  ActionView::Base.send :include, CloudinaryHelper
7
7
  end
8
- end
8
+
9
+ ActiveSupport.on_load(:action_controller_base) do
10
+ ActionController::Base.send :include, Cloudinary::CloudinaryController
11
+ end
12
+ end
@@ -5,8 +5,7 @@ require 'cloudinary/cache'
5
5
 
6
6
  class Cloudinary::Uploader
7
7
 
8
- REMOTE_URL_REGEX = %r(^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$)
9
-
8
+ REMOTE_URL_REGEX = Cloudinary::Utils::REMOTE_URL_REGEX
10
9
  # @deprecated use {Cloudinary::Utils.build_eager} instead
11
10
  def self.build_eager(eager)
12
11
  Cloudinary::Utils.build_eager(eager)
@@ -28,6 +27,7 @@ class Cloudinary::Uploader
28
27
  :backup => Cloudinary::Utils.as_safe_bool(options[:backup]),
29
28
  :callback => options[:callback],
30
29
  :categorization => options[:categorization],
30
+ :cinemagraph_analysis => Cloudinary::Utils.as_safe_bool(options[:cinemagraph_analysis]),
31
31
  :colors => Cloudinary::Utils.as_safe_bool(options[:colors]),
32
32
  :context => Cloudinary::Utils.encode_context(options[:context]),
33
33
  :custom_coordinates => Cloudinary::Utils.encode_double_array(options[:custom_coordinates]),
@@ -37,6 +37,7 @@ class Cloudinary::Uploader
37
37
  :eager_async => Cloudinary::Utils.as_safe_bool(options[:eager_async]),
38
38
  :eager_notification_url => options[:eager_notification_url],
39
39
  :exif => Cloudinary::Utils.as_safe_bool(options[:exif]),
40
+ :eval => options[:eval],
40
41
  :face_coordinates => Cloudinary::Utils.encode_double_array(options[:face_coordinates]),
41
42
  :faces => Cloudinary::Utils.as_safe_bool(options[:faces]),
42
43
  :folder => options[:folder],
@@ -64,6 +65,8 @@ class Cloudinary::Uploader
64
65
  :unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
65
66
  :upload_preset => options[:upload_preset],
66
67
  :use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
68
+ :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis]),
69
+ :metadata => Cloudinary::Utils.encode_context(options[:metadata])
67
70
  }
68
71
  params
69
72
  end
@@ -77,7 +80,10 @@ class Cloudinary::Uploader
77
80
  params = build_upload_params(options)
78
81
  if file.is_a?(Pathname)
79
82
  params[:file] = File.open(file, "rb")
80
- elsif file.respond_to?(:read) || file.match(REMOTE_URL_REGEX)
83
+ elsif file.is_a?(StringIO)
84
+ file.rewind
85
+ params[:file] = Cloudinary::Blob.new(file.read, options)
86
+ elsif file.respond_to?(:read) || Cloudinary::Utils.is_remote?(file)
81
87
  params[:file] = file
82
88
  else
83
89
  params[:file] = File.open(file, "rb")
@@ -95,7 +101,7 @@ class Cloudinary::Uploader
95
101
  public_id = public_id_or_options
96
102
  options = old_options
97
103
  end
98
- if file.match(REMOTE_URL_REGEX)
104
+ if Cloudinary::Utils.is_remote?(file)
99
105
  return upload(file, options.merge(:public_id => public_id))
100
106
  elsif file.is_a?(Pathname) || !file.respond_to?(:read)
101
107
  filename = file
@@ -248,7 +254,7 @@ class Cloudinary::Uploader
248
254
  end
249
255
  end
250
256
 
251
- # options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
257
+ # options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
252
258
  def self.add_tag(tag, public_ids = [], options = {})
253
259
  exclusive = options.delete(:exclusive)
254
260
  command = exclusive ? "set_exclusive" : "add"
@@ -267,6 +273,29 @@ class Cloudinary::Uploader
267
273
  return self.call_tags_api(nil, "remove_all", public_ids, options)
268
274
  end
269
275
 
276
+ # Populates metadata fields with the given values. Existing values will be overwritten.
277
+ #
278
+ # Any metadata-value pairs given are merged with any existing metadata-value pairs
279
+ # (an empty value for an existing metadata field clears the value).
280
+ #
281
+ # @param [Hash] metadata A list of custom metadata fields (by external_id) and the values to assign to each of them.
282
+ # @param [Array] public_ids An array of Public IDs of assets uploaded to Cloudinary.
283
+ # @param [Hash] options
284
+ # @option options [String] :resource_type The type of file. Default: image. Valid values: image, raw, video.
285
+ # @option options [String] :type The storage type. Default: upload. Valid values: upload, private, authenticated
286
+ # @return mixed a list of public IDs that were updated
287
+ # @raise [Cloudinary::Api:Error]
288
+ def self.update_metadata(metadata, public_ids, options = {})
289
+ self.call_api("metadata", options) do
290
+ {
291
+ timestamp: (options[:timestamp] || Time.now.to_i),
292
+ type: options[:type],
293
+ public_ids: Cloudinary::Utils.build_array(public_ids),
294
+ metadata: Cloudinary::Utils.encode_context(metadata)
295
+ }
296
+ end
297
+ end
298
+
270
299
  private
271
300
 
272
301
  def self.call_tags_api(tag, command, public_ids = [], options = {})
@@ -317,7 +346,7 @@ class Cloudinary::Uploader
317
346
  params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
318
347
  params[:api_key] = api_key
319
348
  end
320
- timeout = options[:timeout] || Cloudinary.config.timeout || 60
349
+ timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }
321
350
 
322
351
  result = nil
323
352
 
@@ -1,4 +1,6 @@
1
1
  # Copyright Cloudinary
2
+
3
+ # frozen_string_literal: true
2
4
  require 'digest/sha1'
3
5
  require 'zlib'
4
6
  require 'uri'
@@ -24,7 +26,8 @@ class Cloudinary::Utils
24
26
  "*" => 'mul',
25
27
  "/" => 'div',
26
28
  "+" => 'add',
27
- "-" => 'sub'
29
+ "-" => 'sub',
30
+ "^" => 'pow'
28
31
  }
29
32
 
30
33
  PREDEFINED_VARS = {
@@ -39,9 +42,32 @@ class Cloudinary::Utils
39
42
  "page_x" => "px",
40
43
  "page_y" => "py",
41
44
  "tags" => "tags",
45
+ "initial_duration" => "idu",
46
+ "duration" => "du",
42
47
  "width" => "w"
43
48
  }
44
49
 
50
+ SIMPLE_TRANSFORMATION_PARAMS = {
51
+ :ac => :audio_codec,
52
+ :af => :audio_frequency,
53
+ :br => :bit_rate,
54
+ :cs => :color_space,
55
+ :d => :default_image,
56
+ :dl => :delay,
57
+ :dn => :density,
58
+ :du => :duration,
59
+ :eo => :end_offset,
60
+ :f => :fetch_format,
61
+ :g => :gravity,
62
+ :ki => :keyframe_interval,
63
+ :p => :prefix,
64
+ :pg => :page,
65
+ :so => :start_offset,
66
+ :sp => :streaming_profile,
67
+ :vc => :video_codec,
68
+ :vs => :video_sampling
69
+ }.freeze
70
+
45
71
  URL_KEYS = %w[
46
72
  api_secret
47
73
  auth_token
@@ -113,6 +139,11 @@ class Cloudinary::Utils
113
139
  zoom
114
140
  ].map(&:to_sym)
115
141
 
142
+ REMOTE_URL_REGEX = %r(^ftp:|^https?:|^s3:|^gs:|^data:([\w-]+\/[\w-]+(\+[\w-]+)?)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$)
143
+
144
+ LONG_URL_SIGNATURE_LENGTH = 32
145
+ SHORT_URL_SIGNATURE_LENGTH = 8
146
+
116
147
  def self.extract_config_params(options)
117
148
  options.select{|k,v| URL_KEYS.include?(k)}
118
149
  end
@@ -216,7 +247,7 @@ class Cloudinary::Utils
216
247
  :l => overlay,
217
248
  :o => normalize_expression(options.delete(:opacity)),
218
249
  :q => normalize_expression(options.delete(:quality)),
219
- :r => normalize_expression(options.delete(:radius)),
250
+ :r => process_radius(options.delete(:radius)),
220
251
  :t => named_transformation,
221
252
  :u => underlay,
222
253
  :w => normalize_expression(width),
@@ -224,26 +255,7 @@ class Cloudinary::Utils
224
255
  :y => normalize_expression(options.delete(:y)),
225
256
  :z => normalize_expression(options.delete(:zoom))
226
257
  }
227
- {
228
- :ac => :audio_codec,
229
- :af => :audio_frequency,
230
- :br => :bit_rate,
231
- :cs => :color_space,
232
- :d => :default_image,
233
- :dl => :delay,
234
- :dn => :density,
235
- :du => :duration,
236
- :eo => :end_offset,
237
- :f => :fetch_format,
238
- :g => :gravity,
239
- :ki => :keyframe_interval,
240
- :p => :prefix,
241
- :pg => :page,
242
- :so => :start_offset,
243
- :sp => :streaming_profile,
244
- :vc => :video_codec,
245
- :vs => :video_sampling
246
- }.each do
258
+ SIMPLE_TRANSFORMATION_PARAMS.each do
247
259
  |param, option|
248
260
  params[param] = options.delete(option)
249
261
  end
@@ -293,19 +305,17 @@ class Cloudinary::Utils
293
305
  # Translates the condition if provided.
294
306
  # @return [string] "if_" + ifValue
295
307
  # @private
296
- def self.process_if(ifValue)
297
- if ifValue
298
- ifValue = normalize_expression(ifValue)
299
-
300
- ifValue = "if_" + ifValue
301
- end
308
+ def self.process_if(if_value)
309
+ "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
302
310
  end
303
311
 
304
- EXP_REGEXP = Regexp.new(PREDEFINED_VARS.keys.join("|")+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
312
+ EXP_REGEXP = Regexp.new('(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
305
313
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
306
314
 
307
315
  def self.normalize_expression(expression)
308
- if expression =~ /^!.+!$/ # quoted string
316
+ if expression.nil?
317
+ ''
318
+ elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
309
319
  expression
310
320
  else
311
321
  expression.to_s.gsub(EXP_REGEXP,EXP_REPLACEMENT).gsub(/[ _]+/, "_")
@@ -375,6 +385,17 @@ class Cloudinary::Utils
375
385
  end
376
386
  private_class_method :process_layer
377
387
 
388
+ # Parse radius options
389
+ # @return [string] radius transformation string
390
+ # @private
391
+ def self.process_radius(radius)
392
+ if radius.is_a?(Array) && !radius.length.between?(1, 4)
393
+ raise(CloudinaryException, "Invalid radius parameter")
394
+ end
395
+ Array(radius).map { |r| normalize_expression(r) }.join(":")
396
+ end
397
+ private_class_method :process_radius
398
+
378
399
  LAYER_KEYWORD_PARAMS =[
379
400
  [:font_weight ,"normal"],
380
401
  [:font_style ,"normal"],
@@ -395,6 +416,10 @@ class Cloudinary::Utils
395
416
  keywords.push("letter_spacing_#{letter_spacing}") unless letter_spacing.blank?
396
417
  line_spacing = layer[:line_spacing]
397
418
  keywords.push("line_spacing_#{line_spacing}") unless line_spacing.blank?
419
+ font_antialiasing = layer[:font_antialiasing]
420
+ keywords.push("antialias_#{font_antialiasing}") unless font_antialiasing.blank?
421
+ font_hinting = layer[:font_hinting]
422
+ keywords.push("hinting_#{font_hinting}") unless font_hinting.blank?
398
423
  if !font_size.blank? || !font_family.blank? || !keywords.empty?
399
424
  raise(CloudinaryException, "Must supply font_family for text in overlay/underlay") if font_family.blank?
400
425
  raise(CloudinaryException, "Must supply font_size for text in overlay/underlay") if font_size.blank?
@@ -455,6 +480,7 @@ class Cloudinary::Utils
455
480
 
456
481
  resource_type = options.delete(:resource_type)
457
482
  version = options.delete(:version)
483
+ force_version = config_option_consume(options, :force_version, true)
458
484
  format = options.delete(:format)
459
485
  cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
460
486
 
@@ -474,6 +500,7 @@ class Cloudinary::Utils
474
500
  url_suffix = options.delete(:url_suffix)
475
501
  use_root_path = config_option_consume(options, :use_root_path)
476
502
  auth_token = config_option_consume(options, :auth_token)
503
+ long_url_signature = config_option_consume(options, :long_url_signature)
477
504
  unless auth_token == false
478
505
  auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
479
506
  end
@@ -505,7 +532,12 @@ class Cloudinary::Utils
505
532
  resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
506
533
  source, source_to_sign = finalize_source(source, format, url_suffix)
507
534
 
508
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
535
+ if version.nil? && force_version &&
536
+ source_to_sign.include?("/") &&
537
+ !source_to_sign.match(/^v[0-9]+/) &&
538
+ !source_to_sign.match(/^https?:\//)
539
+ version = 1
540
+ end
509
541
  version &&= "v#{version}"
510
542
 
511
543
  transformation = transformation.gsub(%r(([^:])//), '\1/')
@@ -513,7 +545,7 @@ class Cloudinary::Utils
513
545
  raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
514
546
  to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
515
547
  to_sign = fully_unescape(to_sign)
516
- signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
548
+ signature = compute_signature(to_sign, secret, long_url_signature)
517
549
  end
518
550
 
519
551
  prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -533,7 +565,7 @@ class Cloudinary::Utils
533
565
  source = smart_escape(source)
534
566
  source_to_sign = source
535
567
  else
536
- source = smart_escape(URI.decode(source))
568
+ source = smart_escape(smart_unescape(source))
537
569
  source_to_sign = source
538
570
  unless url_suffix.blank?
539
571
  raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
@@ -704,6 +736,18 @@ class Cloudinary::Utils
704
736
  download_archive_url(options.merge(:target_format => "zip"))
705
737
  end
706
738
 
739
+ # Creates and returns a URL that when invoked creates an archive of a folder.
740
+ #
741
+ # @param [Object] folder_path Full path (from the root) of the folder to download.
742
+ # @param [Hash] options Additional options.
743
+ #
744
+ # @return [String]
745
+ def self.download_folder(folder_path, options = {})
746
+ resource_type = options[:resource_type] || "all"
747
+
748
+ download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
749
+ end
750
+
707
751
  def self.signed_download_url(public_id, options = {})
708
752
  aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path
709
753
  if aws_private_key_path
@@ -737,13 +781,18 @@ class Cloudinary::Utils
737
781
  "#{public_id}#{ext}"
738
782
  end
739
783
 
740
- # Based on CGI::unescape. In addition does not escape / :
784
+ # Based on CGI::escape. In addition does not escape / :
741
785
  def self.smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/)
742
786
  string.gsub(unsafe) do |m|
743
787
  '%' + m.unpack('H2' * m.bytesize).join('%').upcase
744
788
  end
745
789
  end
746
790
 
791
+ # Based on CGI::unescape. In addition keeps '+' character as is
792
+ def self.smart_unescape(string)
793
+ CGI.unescape(string.sub('+', '%2B'))
794
+ end
795
+
747
796
  def self.random_public_id
748
797
  sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
749
798
  sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
@@ -811,7 +860,7 @@ class Cloudinary::Utils
811
860
  end
812
861
  end
813
862
 
814
- IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpg miff pdf png psd svg tif tiff wdp webp zip )
863
+ IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpeg jpg miff pdf png psd svg tif tiff wdp webp zip )
815
864
 
816
865
  AUDIO_FORMATS = %w(aac aifc aiff flac m4a mp3 ogg wav)
817
866
 
@@ -831,10 +880,8 @@ class Cloudinary::Utils
831
880
  case
832
881
  when self.supported_format?(format, IMAGE_FORMATS)
833
882
  'image'
834
- when self.supported_format?(format, VIDEO_FORMATS)
883
+ when self.supported_format?(format, VIDEO_FORMATS), self.supported_format?(format, AUDIO_FORMATS)
835
884
  'video'
836
- when self.supported_format?(format, AUDIO_FORMATS)
837
- 'audio'
838
885
  else
839
886
  'raw'
840
887
  end
@@ -842,7 +889,8 @@ class Cloudinary::Utils
842
889
 
843
890
  def self.config_option_consume(options, option_name, default_value = nil)
844
891
  return options.delete(option_name) if options.include?(option_name)
845
- return Cloudinary.config.send(option_name) || default_value
892
+ option_value = Cloudinary.config.send(option_name)
893
+ option_value.nil? ? default_value : option_value
846
894
  end
847
895
 
848
896
  def self.as_bool(value)
@@ -851,7 +899,7 @@ class Cloudinary::Utils
851
899
  when String then value.downcase == "true" || value == "1"
852
900
  when TrueClass then true
853
901
  when FalseClass then false
854
- when Fixnum then value != 0
902
+ when Integer then value != 0
855
903
  when Symbol then value == :true
856
904
  else
857
905
  raise "Invalid boolean value #{value} of type #{value.class}"
@@ -928,6 +976,7 @@ class Cloudinary::Utils
928
976
  :keep_derived=>Cloudinary::Utils.as_safe_bool(options[:keep_derived]),
929
977
  :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]),
930
978
  :public_ids=>options[:public_ids] && Cloudinary::Utils.build_array(options[:public_ids]),
979
+ :fully_qualified_public_ids=>options[:fully_qualified_public_ids] && Cloudinary::Utils.build_array(options[:fully_qualified_public_ids]),
931
980
  :prefixes=>options[:prefixes] && Cloudinary::Utils.build_array(options[:prefixes]),
932
981
  :expires_at=>options[:expires_at],
933
982
  :transformations => build_eager(options[:transformations]),
@@ -1096,4 +1145,27 @@ class Cloudinary::Utils
1096
1145
  end
1097
1146
  end
1098
1147
 
1148
+ def self.is_remote?(url)
1149
+ REMOTE_URL_REGEX === url
1150
+ end
1151
+
1152
+ # Computes a short or long signature based on a message and secret
1153
+ # @param [String] message The string to sign
1154
+ # @param [String] secret A secret that will be added to the message when signing
1155
+ # @param [Boolean] long_signature Whether to create a short or long signature
1156
+ # @return [String] Properly formatted signature
1157
+ def self.compute_signature(message, secret, long_url_signature)
1158
+ combined_message_secret = message + secret
1159
+
1160
+ algo, signature_length =
1161
+ if long_url_signature
1162
+ [Digest::SHA256, LONG_URL_SIGNATURE_LENGTH]
1163
+ else
1164
+ [Digest::SHA1, SHORT_URL_SIGNATURE_LENGTH]
1165
+ end
1166
+
1167
+ "s--#{Base64.urlsafe_encode64(algo.digest(combined_message_secret))[0, signature_length]}--"
1168
+ end
1169
+
1170
+ private_class_method :compute_signature
1099
1171
  end
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.11.1"
3
+ VERSION = "1.18.0"
4
4
  end
@@ -3,12 +3,33 @@ module CloudinaryHelper
3
3
  DEFAULT_POSTER_OPTIONS = { :format => 'jpg', :resource_type => 'video' }
4
4
  DEFAULT_SOURCE_TYPES = %w(webm mp4 ogv)
5
5
  DEFAULT_VIDEO_OPTIONS = { :resource_type => 'video' }
6
+ DEFAULT_SOURCES = [
7
+ {
8
+ :type => "mp4",
9
+ :codecs => "hev1",
10
+ :transformations => { :video_codec => "h265" }
11
+ },
12
+ {
13
+ :type => "webm",
14
+ :codecs => "vp9",
15
+ :transformations => { :video_codec => "vp9" }
16
+ },
17
+ {
18
+ :type => "mp4",
19
+ :transformations => { :video_codec => "auto" }
20
+ },
21
+ {
22
+ :type => "webm",
23
+ :transformations => { :video_codec => "auto" }
24
+ }
25
+ ]
6
26
 
7
27
  # Creates an HTML video tag for the provided +source+
8
28
  #
9
29
  # ==== Options
10
30
  # * <tt>:source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
11
31
  # * <tt>:source_transformation</tt> - specific transformations to use for a specific source type.
32
+ # * <tt>:sources</tt> - list of sources (overrides :source_types when present)
12
33
  # * <tt>:poster</tt> - override default thumbnail:
13
34
  # * url: provide an ad hoc url
14
35
  # * options: with specific poster transformations and/or Cloudinary +:public_id+
@@ -20,6 +41,17 @@ module CloudinaryHelper
20
41
  # cl_video_tag("mymovie.webm", :source_types => [:webm, :mp4], :poster => {:effect => 'sepia'}) do
21
42
  # content_tag( :span, "Cannot present video!")
22
43
  # end
44
+ # cl_video_tag("mymovie", :sources => [
45
+ # {
46
+ # :type => "mp4",
47
+ # :codecs => "hev1",
48
+ # :transformations => { :video_codec => "h265" }
49
+ # },
50
+ # {
51
+ # :type => "webm",
52
+ # :transformations => { :video_codec => "auto" }
53
+ # }
54
+ # ])
23
55
  def cl_video_tag(source, options = {}, &block)
24
56
  source = strip_known_ext(source)
25
57
  video_attributes = [:autoplay,:controls,:loop,:muted,:poster, :preload]
@@ -48,30 +80,12 @@ module CloudinaryHelper
48
80
  video_options[:poster] = cl_video_thumbnail_path(source, options)
49
81
  end
50
82
 
51
- source_transformation = options.delete(:source_transformation) || {}
52
- source_types = Array(options.delete(:source_types))
53
- fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
83
+ fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
54
84
 
55
- if source_types.size > 1
56
- cloudinary_tag(source, options) do |_source, tag_options|
57
- content_tag('video', tag_options.merge(video_options)) do
58
- source_tags = source_types.map do |type|
59
- transformation = source_transformation[type.to_sym] || {}
60
- cloudinary_tag("#{source}.#{type}", options.merge(transformation)) do |url, _tag_options|
61
- mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
62
- tag("source", :src => url, :type => mime_type)
63
- end
64
- end
65
- source_tags.push(fallback.html_safe) unless fallback.blank?
66
- safe_join(source_tags)
67
- end
68
- end
85
+ if options[:sources]
86
+ video_tag_from_sources(source, options, video_options, fallback)
69
87
  else
70
- transformation = source_transformation[source_types.first.to_sym] || {}
71
- video_options[:src] = cl_video_path("#{source}.#{source_types.first.to_sym}", transformation.merge(options))
72
- cloudinary_tag(source, options) do |_source, tag_options|
73
- content_tag('video', fallback, tag_options.merge(video_options))
74
- end
88
+ video_tag_from_source_types(source, options, video_options, fallback)
75
89
  end
76
90
  end
77
91
 
@@ -96,6 +110,66 @@ module CloudinaryHelper
96
110
  name.sub(/\.(#{DEFAULT_SOURCE_TYPES.join("|")})$/, '')
97
111
  end
98
112
 
113
+ private
114
+
115
+ def video_tag_from_source_types(source_name, options, video_options, fallback)
116
+ source_transformation = options.delete(:source_transformation) || {}
117
+ source_types = Array(options.delete(:source_types))
118
+
119
+ if source_types.size > 1
120
+ sources = source_types.map do |type|
121
+ {
122
+ :type => type,
123
+ :transformations => source_transformation[type.to_sym] || {}
124
+ }
125
+ end
126
+
127
+ generate_tag_from_sources(:source_name => source_name,
128
+ :sources => sources,
129
+ :options => options,
130
+ :video_options => video_options,
131
+ :fallback => fallback)
132
+ else
133
+ transformation = source_transformation[source_types.first.to_sym] || {}
134
+ video_options[:src] = cl_video_path("#{source_name}.#{source_types.first.to_sym}", transformation.merge(options))
135
+ cloudinary_tag(source_name, options) do |_source, tag_options|
136
+ content_tag('video', fallback, tag_options.merge(video_options))
137
+ end
138
+ end
139
+ end
140
+
141
+ def video_tag_from_sources(source_name, options, video_options, fallback)
142
+ sources = options.delete(:sources)
143
+
144
+ generate_tag_from_sources(:source_name => source_name,
145
+ :sources => sources,
146
+ :options => options,
147
+ :video_options => video_options,
148
+ :fallback => fallback)
149
+ end
150
+
151
+ def generate_tag_from_sources(params)
152
+ source_name, sources, options, video_options, fallback = params.values_at(:source_name, :sources, :options, :video_options, :fallback)
153
+
154
+ cloudinary_tag(source_name, options) do |_source, tag_options|
155
+ content_tag('video', tag_options.merge(video_options)) do
156
+ source_tags = sources.map do |source|
157
+ type = source[:type]
158
+ transformation = source[:transformations] || {}
159
+ cloudinary_tag("#{source_name}.#{type}", options.merge(transformation)) do |url, _tag_options|
160
+ mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
161
+ if source[:codecs]
162
+ codecs = source[:codecs].is_a?(Array) ? source[:codecs].join(", ") : source[:codecs]
163
+ mime_type = "#{mime_type}; codecs=#{codecs}"
164
+ end
165
+ tag("source", :src => url, :type => mime_type)
166
+ end
167
+ end
168
+ source_tags.push(fallback.html_safe) unless fallback.blank?
169
+ safe_join(source_tags)
170
+ end
171
+ end
172
+ end
99
173
  end
100
174
 
101
175