cloudinary 1.0.85 → 1.1.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.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZTNjODQ0YjA3MzJjMjNhMmEwNzIyZGExNTRiY2JhMGZjZGYyNTI0MQ==
5
- data.tar.gz: !binary |-
6
- MjhkM2NkNzk1YTExOWQ0ZjdjMTJlNWIzOTE2YzZhMjE2NWZhNDM5OQ==
2
+ SHA1:
3
+ metadata.gz: ed522de3b73b3847f23d46ee3ec165e7cdf20037
4
+ data.tar.gz: 2bda2d2079cf979dd6622e23ec4187dbcf56be34
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- YWM4OGJmYmQxNjc3OGIwYWQ5Y2U4OThhNGJkMDYxNTBlOWNlYzBjZDliYTBm
10
- OWIyZmY1MDI4NzlhODIzZmFmNWJkZDQyYTAyOWZmNzc0MzFjMWRlZDUyZGY5
11
- ZmYwYjQ5ODFiODM0Y2VlNjViN2JjOTJmODI2ODBiNzk4NWViMjk=
12
- data.tar.gz: !binary |-
13
- NTE4NDkyMGU0ZDk3MDNiNjA0OWMwZDZjNTU1OThjNjI3OTQ3Zjg1ZTkxM2U2
14
- MGFjODMxOTBiNDgyYmMyYTdmZmM1Y2FkMWM0ZGZmOWI2MjM3NjUzY2E4ZDhk
15
- YTc5OGY1OWIzNTIzYjcwYzRmNmMzZThiZjcwMTI4MTQyOTU3NmY=
6
+ metadata.gz: d75f033f6b0a02263f626b9f2f2c57de96fcf773eb455079fb647bf50c447f8280138eeb93298ba67a4d22f34c42f1f5ca29af1eb108dbe303fc19efd93b90ac
7
+ data.tar.gz: c815b8e4af15df396cc425ab505608c577c7f2616d76101041bf9c41f1561aea69fd7676a14294ef7df418d86c2375e044db6c7e6630b4d95b48c5f300e8fa30
data/.gitignore CHANGED
@@ -3,9 +3,9 @@
3
3
  /.config
4
4
  capybara-*.html
5
5
  .rspec
6
- log/
7
- db/*.sqlite3
8
- db/*.sqlite3-journal
6
+ /log
7
+ /db/*.sqlite3
8
+ /db/*.sqlite3-journal
9
9
  /public/system
10
10
  /coverage/
11
11
  /InstalledFiles
@@ -14,7 +14,7 @@ db/*.sqlite3-journal
14
14
  /test/tmp/
15
15
  /spec/tmp
16
16
  /test/version_tmp/
17
- tmp/
17
+ /tmp
18
18
  **.orig
19
19
  rerun.txt
20
20
  pickle-email-*.html
@@ -49,7 +49,7 @@ Gemfile.lock
49
49
  .rvmrc
50
50
 
51
51
  # if using bower-rails ignore default bower_components path bower.json files
52
- vendor/assets/bower_components
52
+ /vendor/assets/bower_components
53
53
  *.bowerrc
54
54
  bower.json
55
55
 
@@ -1,3 +1,16 @@
1
+ # Version 1.1.0 - 2015-04-21
2
+ * Pull request #136 - Update `process.rb` to ensure name value is an array.
3
+ * CarrierWave
4
+ * Store `resource_type` and `type` (aka `storage_type`) in carrierwave column for better support of non-image resource types and non-upload types
5
+ * only pass format to Cloudinary when explicitly requested
6
+ * Support disabling new extended identifier format
7
+ * Use upload endpoint instead of upload_chunked
8
+ * Remove `symoblize_keys` monkey patching
9
+ * Update Rspec dependency.
10
+ * Fix markup in the readme file.
11
+ * Add `.gitignore` to each sample project. Add files required for testing.
12
+ * Fix changelog format (missing newline)
13
+
1
14
  # Version 1.0.85 - 2015-04-08
2
15
  * Remove symoblize_keys intrusive implementation.
3
16
  * Use upload API endpoint instead of upload_chunked.
data/README.md CHANGED
@@ -12,13 +12,13 @@ Cloudinary provides URL and HTTP based APIs that can be easily integrated with a
12
12
  For Ruby on Rails, Cloudinary provides a GEM for simplifying the integration even further.
13
13
 
14
14
  ## Getting started guide
15
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide of Ruby on Rails](http://cloudinary.com/documentation/rails_integration#getting_started_guide)**.
15
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide of Ruby on Rails](http://cloudinary.com/documentation/rails_integration#getting_started_guide)**.
16
16
 
17
17
  ## Setup ######################################################################
18
18
 
19
19
  To install the Cloudinary Ruby GEM, run:
20
20
 
21
- $ gem install cloudinary
21
+ $ gem install cloudinary
22
22
 
23
23
  If you use Rails 3.x or higher, edit your `Gemfile`, add the following line and run `bundle install`
24
24
 
@@ -117,7 +117,7 @@ Same goes for Twitter:
117
117
 
118
118
  twitter_name_profile_image_tag("billclinton.jpg")
119
119
 
120
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_manipulation) for more information about displaying and transforming images in Rails**.
120
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_manipulation) for more information about displaying and transforming images in Rails**.
121
121
 
122
122
 
123
123
 
@@ -144,11 +144,14 @@ You can also specify your own public ID:
144
144
  http://res.cloudinary.com/demo/image/upload/sample_remote.jpg
145
145
 
146
146
 
147
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_upload) for plenty more options of uploading to the cloud from your Ruby code or directly from the browser**.
147
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_upload) for plenty more options of uploading to the cloud from your Ruby code or directly from the browser**.
148
148
 
149
149
 
150
150
  ### CarrierWave Integration
151
151
 
152
+ **Note:** Starting from version 1.1.0 the CarrierWave database format has changed to include the resource type and storage type. The new functionality
153
+ is backward compatible with the previous format. To use the old format override `use_extended_identifier?` in the Uploader and return `false`.
154
+
152
155
  Cloudinary's Ruby GEM includes an optional plugin for [CarrierWave](https://github.com/jnicklas/carrierwave). If you already use CarrierWave, simply include `Cloudinary::CarrierWave` to switch to cloud storage and image processing in the cloud.
153
156
 
154
157
  class PictureUploader < CarrierWave::Uploader::Base
@@ -156,7 +159,7 @@ Cloudinary's Ruby GEM includes an optional plugin for [CarrierWave](https://gith
156
159
  ...
157
160
  end
158
161
 
159
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **For more details on CarrierWave integration see [our documentation](http://cloudinary.com/documentation/rails_carrierwave)**.
162
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **For more details on CarrierWave integration see [our documentation](http://cloudinary.com/documentation/rails_carrierwave)**.
160
163
 
161
164
  We also published an interesting blog post about [Ruby on Rails image uploads with CarrierWave and Cloudinary](http://cloudinary.com/blog/ruby_on_rails_image_uploads_with_carrierwave_and_cloudinary).
162
165
 
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ task :default => :spec
7
7
  Bundler::GemHelper.install_tasks
8
8
 
9
9
  task :fetch_assets do
10
- system "/bin/rm -rf vendor/assets; mkdir -p vendor/assets; cd vendor/assets; curl -L https://github.com/cloudinary/cloudinary_js/archive/1.0.23.tar.gz | tar zxvf - --strip=1"
10
+ system "/bin/rm -rf vendor/assets; mkdir -p vendor/assets; cd vendor/assets; curl -L https://github.com/cloudinary/cloudinary_js/tarball/master | tar zxvf - --strip=1"
11
11
  system "mkdir -p vendor/assets/javascripts; mv vendor/assets/js vendor/assets/javascripts/cloudinary"
12
12
  File.open("vendor/assets/javascripts/cloudinary/index.js", "w") do
13
13
  |f|
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ["lib"]
22
22
 
23
23
  s.add_dependency "aws_cf_signer"
24
- s.add_development_dependency "rspec", '>=2.11'
24
+ s.add_development_dependency "rspec", '>=3.2'
25
25
  s.add_development_dependency "rspec-rails"
26
26
 
27
27
  if RUBY_VERSION > "1.9"
@@ -135,6 +135,11 @@ module Cloudinary::CarrierWave
135
135
  def auto_rename_preloaded?
136
136
  true
137
137
  end
138
+
139
+ # Use extended identifier format that includes resource type and storage type.
140
+ def use_extended_identifier?
141
+ true
142
+ end
138
143
 
139
144
  class CloudinaryFile
140
145
  attr_reader :identifier, :public_id, :filename, :format, :version, :storage_type, :resource_type
@@ -142,7 +147,12 @@ module Cloudinary::CarrierWave
142
147
  @uploader = uploader
143
148
  @identifier = identifier
144
149
 
145
- if @identifier.match(%r(^v([0-9]+)/(.*)))
150
+ if @identifier.match(%r(^(image|raw|video)/(upload|private|authenticated)(?:/v([0-9]+))?/(.*)))
151
+ @resource_type = $1
152
+ @storage_type = $2
153
+ @version = $3.presence
154
+ @filename = $4
155
+ elsif @identifier.match(%r(^v([0-9]+)/(.*)))
146
156
  @version = $1
147
157
  @filename = $2
148
158
  else
@@ -150,8 +160,8 @@ module Cloudinary::CarrierWave
150
160
  @version = nil
151
161
  end
152
162
 
153
- @storage_type = uploader.class.storage_type
154
- @resource_type = Cloudinary::Utils.resource_type_for_format(@filename)
163
+ @storage_type ||= uploader.class.storage_type
164
+ @resource_type ||= Cloudinary::Utils.resource_type_for_format(@filename)
155
165
  @public_id, @format = Cloudinary::PreloadedFile.split_format(@filename)
156
166
  end
157
167
 
@@ -180,8 +190,12 @@ module Cloudinary::CarrierWave
180
190
  "png"
181
191
  end
182
192
 
193
+ def storage_type
194
+ @file.respond_to?(:storage_type) ? @file.storage_type : self.class.storage_type
195
+ end
196
+
183
197
  def resource_type
184
- Cloudinary::Utils.resource_type_for_format(requested_format || original_filename || default_format)
198
+ @file.respond_to?(:resource_type) ? @file.resource_type : Cloudinary::Utils.resource_type_for_format(requested_format || original_filename || default_format)
185
199
  end
186
200
 
187
201
  # For the given methods - versions should call the main uploader method
@@ -105,7 +105,7 @@ module Cloudinary::CarrierWave
105
105
  end
106
106
  else
107
107
  if args.blank?
108
- send(name).each do
108
+ Array(send(name)).each do
109
109
  |attr, value|
110
110
  set_or_yell(@transformation, attr, value)
111
111
  end
@@ -11,11 +11,11 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
11
11
  @stored_version = file.version
12
12
  uploader.rename(nil, true)
13
13
  else
14
- store_cloudinary_identifier(file.version, file.filename)
14
+ store_cloudinary_identifier(file.version, file.filename, file.resource_type, file.type)
15
15
  end
16
- return
16
+ return # Nothing to do
17
17
  when Cloudinary::CarrierWave::CloudinaryFile, Cloudinary::CarrierWave::StoredFile
18
- return nil # Nothing to do
18
+ return # Nothing to do
19
19
  when Cloudinary::CarrierWave::RemoteFile
20
20
  data = file.uri.to_s
21
21
  else
@@ -26,7 +26,7 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
26
26
  # This is the toplevel, need to upload the actual file.
27
27
  params = uploader.transformation.dup
28
28
  params[:return_error] = true
29
- params[:format] = uploader.format
29
+ params[:format] = uploader.requested_format
30
30
  params[:public_id] = uploader.my_public_id
31
31
  uploader.versions.values.each(&:tags) # Validate no tags in versions
32
32
  params[:tags] = uploader.tags if uploader.tags
@@ -42,7 +42,7 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
42
42
 
43
43
  if uploader.metadata["version"]
44
44
  filename = [uploader.metadata["public_id"], uploader.metadata["format"]].reject(&:blank?).join(".")
45
- store_cloudinary_identifier(uploader.metadata["version"], filename)
45
+ store_cloudinary_identifier(uploader.metadata["version"], filename, uploader.metadata["resource_type"], uploader.metadata["type"])
46
46
  end
47
47
  # Will throw an exception on error
48
48
  else
@@ -52,24 +52,18 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
52
52
  nil
53
53
  end
54
54
 
55
- # @deprecated For backward compatibility
56
- def store_cloudinary_version(version)
57
- if identifier.match(%r(^(v[0-9]+)/(.*)))
58
- filename = $2
59
- else
60
- filename = identifier
61
- end
62
-
63
- store_cloudinary_identifier(version, filename)
64
- end
65
-
66
55
  # Updates the model mounter identifier with version information.
67
56
  #
68
57
  # Carrierwave uses hooks when integrating with ORMs so it's important to
69
58
  # update the identifier in a way that does not trigger hooks again or else
70
59
  # you'll get stuck in a loop.
71
- def store_cloudinary_identifier(version, filename)
60
+ def store_cloudinary_identifier(version, filename, resource_type=nil, type=nil)
72
61
  name = "v#{version}/#{filename}"
62
+ if uploader.use_extended_identifier?
63
+ resource_type ||= uploader.resource_type || "image"
64
+ type ||= uploader.storage_type || "upload"
65
+ name = "#{resource_type}/#{type}/#{name}"
66
+ end
73
67
  model_class = uploader.model.class
74
68
  column = uploader.model.send(:_mounter, uploader.mounted_as).send(:serialization_column)
75
69
  if defined?(ActiveRecord::Base) && uploader.model.is_a?(ActiveRecord::Base)
@@ -1,551 +1,554 @@
1
- # Copyright Cloudinary
2
- require 'digest/sha1'
3
- require 'zlib'
4
- require 'uri'
5
- require 'aws_cf_signer'
6
-
7
- class Cloudinary::Utils
8
- # @deprecated Use Cloudinary::SHARED_CDN
9
- SHARED_CDN = Cloudinary::SHARED_CDN
10
- DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
11
-
12
- # Warning: options are being destructively updated!
13
- def self.generate_transformation_string(options={})
14
- if options.is_a?(Array)
15
- return options.map{|base_transformation| generate_transformation_string(base_transformation.clone)}.join("/")
16
- end
17
- # Symbolize keys
18
- options.keys.each do |key|
19
- options[(key.to_sym rescue key)] = options.delete(key)
20
- end
21
-
22
- responsive_width = config_option_consume(options, :responsive_width)
23
- size = options.delete(:size)
24
- options[:width], options[:height] = size.split("x") if size
25
- width = options[:width]
26
- width = width.to_s if width.is_a?(Symbol)
27
- height = options[:height]
28
- has_layer = options[:overlay].present? || options[:underlay].present?
29
-
30
- crop = options.delete(:crop)
31
- angle = build_array(options.delete(:angle)).join(".")
32
-
33
- no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
34
- options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width == "auto" || responsive_width)
35
- options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)
36
-
37
- width=height=nil if crop.nil? && !has_layer && width != "auto"
38
-
39
- background = options.delete(:background)
40
- background = background.sub(/^#/, 'rgb:') if background
41
-
42
- color = options.delete(:color)
43
- color = color.sub(/^#/, 'rgb:') if color
44
-
45
- base_transformations = build_array(options.delete(:transformation))
46
- if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
47
- base_transformations = base_transformations.map do
48
- |base_transformation|
49
- base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone) : generate_transformation_string(:transformation=>base_transformation)
50
- end
51
- else
52
- named_transformation = base_transformations.join(".")
53
- base_transformations = []
54
- end
55
-
56
- effect = options.delete(:effect)
57
- effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
58
-
59
- border = options.delete(:border)
60
- if border.is_a?(Hash)
61
- border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
62
- elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
63
- options[:border] = border
64
- border = nil
65
- end
66
- flags = build_array(options.delete(:flags)).join(".")
67
- dpr = config_option_consume(options, :dpr)
68
-
69
- if options.include? :offset
70
- options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
71
- end
72
-
73
- params = {
74
- :a => angle,
75
- :b => background,
76
- :bo => border,
77
- :c => crop,
78
- :co => color,
79
- :dpr => dpr,
80
- :e => effect,
81
- :fl => flags,
82
- :h => height,
83
- :t => named_transformation,
84
- :w => width
85
- }
86
- {
87
- :ac => :audio_codec,
88
- :br => :bit_rate,
89
- :cs => :color_space,
90
- :d => :default_image,
91
- :dl => :delay,
92
- :dn => :density,
93
- :du => :duration,
94
- :eo => :end_offset,
95
- :f => :fetch_format,
96
- :g => :gravity,
97
- :l => :overlay,
98
- :o => :opacity,
99
- :p => :prefix,
100
- :pg => :page,
101
- :q => :quality,
102
- :r => :radius,
103
- :af => :audio_frequency,
104
- :so => :start_offset,
105
- :u => :underlay,
106
- :vc => :video_codec,
107
- :vs => :video_sampling,
108
- :x => :x,
109
- :y => :y,
110
- :z => :zoom
111
- }.each do
112
- |param, option|
113
- params[param] = options.delete(option)
114
- end
115
-
116
- params[:vc] = process_video_params params[:vc] if params[:vc].present?
117
- [:so, :eo, :du].each do |range_value|
118
- params[range_value] = norm_range_value params[range_value] if params[range_value].present?
119
- end
120
-
121
- transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort.join(",")
122
- raw_transformation = options.delete(:raw_transformation)
123
- transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
124
- transformations = base_transformations << transformation
125
- if responsive_width
126
- responsive_width_transformation = Cloudinary.config.responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
127
- transformations << generate_transformation_string(responsive_width_transformation.clone)
128
- end
129
-
130
- if width.to_s == "auto" || responsive_width
131
- options[:responsive] = true
132
- end
133
- if dpr.to_s == "auto"
134
- options[:hidpi] = true
135
- end
136
-
137
- transformations.reject(&:blank?).join("/")
138
- end
139
-
140
- def self.api_string_to_sign(params_to_sign)
141
- params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
142
- end
143
-
144
- def self.api_sign_request(params_to_sign, api_secret)
145
- to_sign = api_string_to_sign(params_to_sign)
146
- Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
147
- end
148
-
149
- # Warning: options are being destructively updated!
150
- def self.unsigned_download_url(source, options = {})
151
-
152
- type = options.delete(:type)
153
-
154
- options[:fetch_format] ||= options.delete(:format) if type == :fetch
155
- transformation = self.generate_transformation_string(options)
156
-
157
- resource_type = options.delete(:resource_type) || "image"
158
- version = options.delete(:version)
159
- format = options.delete(:format)
160
- cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
161
-
162
- secure = options.delete(:secure)
163
- ssl_detected = options.delete(:ssl_detected)
164
- secure = ssl_detected || Cloudinary.config.secure if secure.nil?
165
- private_cdn = config_option_consume(options, :private_cdn)
166
- secure_distribution = config_option_consume(options, :secure_distribution)
167
- cname = config_option_consume(options, :cname)
168
- shorten = config_option_consume(options, :shorten)
169
- force_remote = options.delete(:force_remote)
170
- cdn_subdomain = config_option_consume(options, :cdn_subdomain)
171
- secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
172
- sign_url = config_option_consume(options, :sign_url)
173
- secret = config_option_consume(options, :api_secret)
174
- sign_version = config_option_consume(options, :sign_version) # Deprecated behavior
175
- url_suffix = options.delete(:url_suffix)
176
- use_root_path = config_option_consume(options, :use_root_path)
177
-
178
- raise(CloudinaryException, "URL Suffix only supported in private CDN") if url_suffix.present? and not private_cdn
179
-
180
- original_source = source
181
- return original_source if source.blank?
182
- if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
183
- source = format.blank? ? source.filename : source.full_public_id
184
- end
185
- source = source.to_s
186
- if !force_remote
187
- return original_source if (type.nil? || type == :asset) && source.match(%r(^https?:/)i)
188
- if source.start_with?("/")
189
- if source.start_with?("/images/")
190
- source = source.sub(%r(/images/), '')
191
- else
192
- return original_source
193
- end
194
- end
195
- @metadata ||= defined?(Cloudinary::Static) ? Cloudinary::Static.metadata : {}
196
- if type == :asset && @metadata["images/#{source}"]
197
- return original_source if !Cloudinary.config.static_image_support
198
- source = @metadata["images/#{source}"]["public_id"]
199
- source += File.extname(original_source) if !format
200
- elsif type == :asset
201
- return original_source # requested asset, but no metadata - probably local file. return.
202
- end
203
- end
204
-
205
- resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
206
- source, source_to_sign = finalize_source(source, format, url_suffix)
207
-
208
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
209
- version &&= "v#{version}"
210
-
211
- transformation = transformation.gsub(%r(([^:])//), '\1/')
212
- if sign_url
213
- to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
214
- signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
215
- end
216
-
217
- prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
218
- source = [prefix, resource_type, type, signature, transformation, version, source].reject(&:blank?).join("/")
219
- end
220
-
221
- def self.finalize_source(source, format, url_suffix)
222
- source = source.gsub(%r(([^:])//), '\1/')
223
- if source.match(%r(^https?:/)i)
224
- source = smart_escape(source)
225
- source_to_sign = source
226
- else
227
- source = smart_escape(URI.decode(source))
228
- source_to_sign = source
229
- unless url_suffix.blank?
230
- raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
231
- source = "#{source}/#{url_suffix}"
232
- end
233
- if !format.blank?
234
- source = "#{source}.#{format}"
235
- source_to_sign = "#{source_to_sign}.#{format}"
236
- end
237
- end
238
- [source, source_to_sign]
239
- end
240
-
241
- def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
242
- type ||= :upload
243
- if !url_suffix.blank?
244
- if resource_type.to_s == "image" && type.to_s == "upload"
245
- resource_type = "images"
246
- type = nil
247
- elsif resource_type.to_s == "raw" && type.to_s == "upload"
248
- resource_type = "files"
249
- type = nil
250
- else
251
- raise(CloudinaryException, "URL Suffix only supported for image/upload and raw/upload")
252
- end
253
- end
254
- if use_root_path
255
- if (resource_type.to_s == "image" && type.to_s == "upload") || (resource_type.to_s == "images" && type.blank?)
256
- resource_type = nil
257
- type = nil
258
- else
259
- raise(CloudinaryException, "Root path only supported for image/upload")
260
- end
261
- end
262
- if shorten && resource_type.to_s == "image" && type.to_s == "upload"
263
- resource_type = "iu"
264
- type = nil
265
- end
266
- [resource_type, type]
267
- end
268
-
269
- # cdn_subdomain and secure_cdn_subdomain
270
- # 1) Customers in shared distribution (e.g. res.cloudinary.com)
271
- # if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
272
- # 2) Customers with private cdn
273
- # if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
274
- # if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
275
- # 3) Customers with cname
276
- # if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
277
- def self.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
278
- return "/res#{cloud_name}" if cloud_name.start_with?("/") # For development
279
-
280
- shared_domain = !private_cdn
281
-
282
- if secure
283
- if secure_distribution.nil? || secure_distribution == Cloudinary::OLD_AKAMAI_SHARED_CDN
284
- secure_distribution = private_cdn ? "#{cloud_name}-res.cloudinary.com" : Cloudinary::SHARED_CDN
285
- end
286
- shared_domain ||= secure_distribution == Cloudinary::SHARED_CDN
287
- secure_cdn_subdomain = cdn_subdomain if secure_cdn_subdomain.nil? && shared_domain
288
-
289
- if secure_cdn_subdomain
290
- secure_distribution = secure_distribution.gsub('res.cloudinary.com', "res-#{(Zlib::crc32(source) % 5) + 1}.cloudinary.com")
291
- end
292
-
293
- prefix = "https://#{secure_distribution}"
294
- elsif cname
295
- subdomain = cdn_subdomain ? "a#{(Zlib::crc32(source) % 5) + 1}." : ""
296
- prefix = "http://#{subdomain}#{cname}"
297
- else
298
- host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
299
- prefix = "http://#{host}"
300
- end
301
- prefix += "/#{cloud_name}" if shared_domain
302
-
303
- prefix
304
- end
305
-
306
- def self.cloudinary_api_url(action = 'upload', options = {})
307
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
308
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
309
- resource_type = options[:resource_type] || "image"
310
- return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
311
- end
312
-
313
- def self.sign_request(params, options={})
314
- api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
315
- api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
316
- params = params.reject{|k, v| self.safe_blank?(v)}
317
- params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
318
- params[:api_key] = api_key
319
- params
320
- end
321
-
322
- def self.private_download_url(public_id, format, options = {})
323
- cloudinary_params = sign_request({
324
- :timestamp=>Time.now.to_i,
325
- :public_id=>public_id,
326
- :format=>format,
327
- :type=>options[:type],
328
- :attachment=>options[:attachment],
329
- :expires_at=>options[:expires_at] && options[:expires_at].to_i
330
- }, options)
331
-
332
- return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
333
- end
334
-
335
- def self.zip_download_url(tag, options = {})
336
- cloudinary_params = sign_request({:timestamp=>Time.now.to_i, :tag=>tag, :transformation=>generate_transformation_string(options)}, options)
337
- return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
338
- end
339
-
340
- def self.signed_download_url(public_id, options = {})
341
- aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path || raise(CloudinaryException, "Must supply aws_private_key_path")
342
- aws_key_pair_id = options[:aws_key_pair_id] || Cloudinary.config.aws_key_pair_id || raise(CloudinaryException, "Must supply aws_key_pair_id")
343
- authenticated_distribution = options[:authenticated_distribution] || Cloudinary.config.authenticated_distribution || raise(CloudinaryException, "Must supply authenticated_distribution")
344
- @signers ||= Hash.new{|h,k| path, id = k; h[k] = AwsCfSigner.new(path, id)}
345
- signer = @signers[[aws_private_key_path, aws_key_pair_id]]
346
- url = Cloudinary::Utils.unsigned_download_url(public_id, {:type=>:authenticated}.merge(options).merge(:secure=>true, :secure_distribution=>authenticated_distribution, :private_cdn=>true))
347
- expires_at = options[:expires_at] || (Time.now+3600)
348
- signer.sign(url, :ending => expires_at)
349
- end
350
-
351
- def self.cloudinary_url(public_id, options = {})
352
- if options[:type].to_s == 'authenticated' && !options[:sign_url]
353
- result = signed_download_url(public_id, options)
354
- else
355
- result = unsigned_download_url(public_id, options)
356
- end
357
- return result
358
- end
359
-
360
- def self.asset_file_name(path)
361
- data = Cloudinary.app_root.join(path).read(:mode=>"rb")
362
- ext = path.extname
363
- md5 = Digest::MD5.hexdigest(data)
364
- public_id = "#{path.basename(ext)}-#{md5}"
365
- "#{public_id}#{ext}"
366
- end
367
-
368
- # Based on CGI::unescape. In addition does not escape / :
369
- def self.smart_escape(string)
370
- string.gsub(/([^a-zA-Z0-9_.\-\/:]+)/) do
371
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
372
- end
373
- end
374
-
375
- def self.random_public_id
376
- sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
377
- sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
378
- end
379
-
380
- def self.signed_preloaded_image(result)
381
- "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
382
- end
383
-
384
- @@json_decode = false
385
- def self.json_decode(str)
386
- if !@@json_decode
387
- @@json_decode = true
388
- begin
389
- require 'json'
390
- rescue LoadError
391
- begin
392
- require 'active_support/json'
393
- rescue LoadError
394
- raise LoadError, "Please add the json gem or active_support to your Gemfile"
395
- end
396
- end
397
- end
398
- defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
399
- end
400
-
401
- def self.build_array(array)
402
- case array
403
- when Array then array
404
- when nil then []
405
- else [array]
406
- end
407
- end
408
-
409
- def self.encode_hash(hash)
410
- case hash
411
- when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
412
- when nil then ""
413
- else hash
414
- end
415
- end
416
-
417
- def self.encode_double_array(array)
418
- array = build_array(array)
419
- if array.length > 0 && array[0].is_a?(Array)
420
- return array.map{|a| build_array(a).join(",")}.join("|")
421
- else
422
- return array.join(",")
423
- end
424
- end
425
-
426
- IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
427
-
428
- def self.supported_image_format?(format)
429
- format = format.to_s.downcase
430
- extension = format =~ /\./ ? format.split('.').last : format
431
- IMAGE_FORMATS.include?(extension)
432
- end
433
-
434
- def self.resource_type_for_format(format)
435
- self.supported_image_format?(format) ? 'image' : 'raw'
436
- end
437
-
438
- def self.config_option_consume(options, option_name, default_value = nil)
439
- return options.delete(option_name) if options.include?(option_name)
440
- return Cloudinary.config.send(option_name) || default_value
441
- end
442
-
443
- def self.as_bool(value)
444
- case value
445
- when nil then nil
446
- when String then value.downcase == "true" || value == "1"
447
- when TrueClass then true
448
- when FalseClass then false
449
- when Fixnum then value != 0
450
- when Symbol then value == :true
451
- else
452
- raise "Invalid boolean value #{value} of type #{value.class}"
453
- end
454
- end
455
-
456
- def self.as_safe_bool(value)
457
- case as_bool(value)
458
- when nil then nil
459
- when TrueClass then 1
460
- when FalseClass then 0
461
- end
462
- end
463
-
464
- def self.safe_blank?(value)
465
- value.nil? || value == "" || value == []
466
- end
467
-
468
- private
469
- def self.number_pattern
470
- "([0-9]*)\\.([0-9]+)|([0-9]+)"
471
- end
472
-
473
- def self.offset_any_pattern
474
- "(#{number_pattern})([%pP])?"
475
- end
476
-
477
- def self.offset_any_pattern_re
478
- /((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)\.\.((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)/
479
- end
480
-
481
- # Split a range into the start and end values
482
- def self.split_range(range) # :nodoc:
483
- case range
484
- when Range
485
- [range.first, range.last]
486
- when String
487
- range.split ".." if offset_any_pattern_re =~ range
488
- when Array
489
- [range.first, range.last]
490
- else
491
- nil
492
- end
493
- end
494
-
495
- # Normalize an offset value
496
- # @param [String] value a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
497
- # @return [Object|String] a normalized String of the input value if possible otherwise the value itself
498
- def self.norm_range_value(value) # :nodoc:
499
- offset = /^#{offset_any_pattern}$/.match( value.to_s)
500
- if offset
501
- modifier = offset[5].present? ? 'p' : ''
502
- value = "#{offset[1]}#{modifier}"
503
- end
504
- value
505
- end
506
-
507
- # A video codec parameter can be either a String or a Hash.
508
- #
509
- # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
510
- # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
511
- # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
512
- # or the param if a String was provided.
513
- # Returns NIL if param is not a Hash or String
514
- def self.process_video_params(param)
515
- case param
516
- when Hash
517
- video = ""
518
- if param.has_key? :codec
519
- video = param[:codec]
520
- if param.has_key? :profile
521
- video.concat ":" + param[:profile]
522
- if param.has_key? :level
523
- video.concat ":" + param[:level]
524
- end
525
- end
526
- end
527
- video
528
- when String
529
- param
530
- else
531
- nil
532
- end
533
- end
534
-
535
- def self.deep_symbolize_keys(object)
536
- case object
537
- when Hash
538
- result = {}
539
- object.each do |key, value|
540
- key = key.to_sym rescue key
541
- result[key] = deep_symbolize_keys(value)
542
- end
543
- result
544
- when Array
545
- object.map{|e| deep_symbolize_keys(e)}
546
- else
547
- object
548
- end
549
- end
550
-
551
- end
1
+ # Copyright Cloudinary
2
+ require 'digest/sha1'
3
+ require 'zlib'
4
+ require 'uri'
5
+ require 'aws_cf_signer'
6
+
7
+ class Cloudinary::Utils
8
+ # @deprecated Use Cloudinary::SHARED_CDN
9
+ SHARED_CDN = Cloudinary::SHARED_CDN
10
+ DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
11
+
12
+ # Warning: options are being destructively updated!
13
+ def self.generate_transformation_string(options={})
14
+ if options.is_a?(Array)
15
+ return options.map{|base_transformation| generate_transformation_string(base_transformation.clone)}.join("/")
16
+ end
17
+ # Symbolize keys
18
+ options.keys.each do |key|
19
+ options[(key.to_sym rescue key)] = options.delete(key)
20
+ end
21
+
22
+ responsive_width = config_option_consume(options, :responsive_width)
23
+ size = options.delete(:size)
24
+ options[:width], options[:height] = size.split("x") if size
25
+ width = options[:width]
26
+ width = width.to_s if width.is_a?(Symbol)
27
+ height = options[:height]
28
+ has_layer = options[:overlay].present? || options[:underlay].present?
29
+
30
+ crop = options.delete(:crop)
31
+ angle = build_array(options.delete(:angle)).join(".")
32
+
33
+ no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
34
+ options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width == "auto" || responsive_width)
35
+ options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)
36
+
37
+ width=height=nil if crop.nil? && !has_layer && width != "auto"
38
+
39
+ background = options.delete(:background)
40
+ background = background.sub(/^#/, 'rgb:') if background
41
+
42
+ color = options.delete(:color)
43
+ color = color.sub(/^#/, 'rgb:') if color
44
+
45
+ base_transformations = build_array(options.delete(:transformation))
46
+ if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
47
+ base_transformations = base_transformations.map do
48
+ |base_transformation|
49
+ base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone) : generate_transformation_string(:transformation=>base_transformation)
50
+ end
51
+ else
52
+ named_transformation = base_transformations.join(".")
53
+ base_transformations = []
54
+ end
55
+
56
+ effect = options.delete(:effect)
57
+ effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
58
+
59
+ border = options.delete(:border)
60
+ if border.is_a?(Hash)
61
+ border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
62
+ elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
63
+ options[:border] = border
64
+ border = nil
65
+ end
66
+ flags = build_array(options.delete(:flags)).join(".")
67
+ dpr = config_option_consume(options, :dpr)
68
+
69
+ if options.include? :offset
70
+ options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
71
+ end
72
+
73
+ params = {
74
+ :a => angle,
75
+ :b => background,
76
+ :bo => border,
77
+ :c => crop,
78
+ :co => color,
79
+ :dpr => dpr,
80
+ :e => effect,
81
+ :fl => flags,
82
+ :h => height,
83
+ :t => named_transformation,
84
+ :w => width
85
+ }
86
+ {
87
+ :ac => :audio_codec,
88
+ :br => :bit_rate,
89
+ :cs => :color_space,
90
+ :d => :default_image,
91
+ :dl => :delay,
92
+ :dn => :density,
93
+ :du => :duration,
94
+ :eo => :end_offset,
95
+ :f => :fetch_format,
96
+ :g => :gravity,
97
+ :l => :overlay,
98
+ :o => :opacity,
99
+ :p => :prefix,
100
+ :pg => :page,
101
+ :q => :quality,
102
+ :r => :radius,
103
+ :af => :audio_frequency,
104
+ :so => :start_offset,
105
+ :u => :underlay,
106
+ :vc => :video_codec,
107
+ :vs => :video_sampling,
108
+ :x => :x,
109
+ :y => :y,
110
+ :z => :zoom
111
+ }.each do
112
+ |param, option|
113
+ params[param] = options.delete(option)
114
+ end
115
+
116
+ params[:vc] = process_video_params params[:vc] if params[:vc].present?
117
+ [:so, :eo, :du].each do |range_value|
118
+ params[range_value] = norm_range_value params[range_value] if params[range_value].present?
119
+ end
120
+
121
+ transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort.join(",")
122
+ raw_transformation = options.delete(:raw_transformation)
123
+ transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
124
+ transformations = base_transformations << transformation
125
+ if responsive_width
126
+ responsive_width_transformation = Cloudinary.config.responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
127
+ transformations << generate_transformation_string(responsive_width_transformation.clone)
128
+ end
129
+
130
+ if width.to_s == "auto" || responsive_width
131
+ options[:responsive] = true
132
+ end
133
+ if dpr.to_s == "auto"
134
+ options[:hidpi] = true
135
+ end
136
+
137
+ transformations.reject(&:blank?).join("/")
138
+ end
139
+
140
+ def self.api_string_to_sign(params_to_sign)
141
+ params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
142
+ end
143
+
144
+ def self.api_sign_request(params_to_sign, api_secret)
145
+ to_sign = api_string_to_sign(params_to_sign)
146
+ Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
147
+ end
148
+
149
+ # Warning: options are being destructively updated!
150
+ def self.unsigned_download_url(source, options = {})
151
+
152
+ type = options.delete(:type)
153
+
154
+ options[:fetch_format] ||= options.delete(:format) if type == :fetch
155
+ transformation = self.generate_transformation_string(options)
156
+
157
+ resource_type = options.delete(:resource_type)
158
+ version = options.delete(:version)
159
+ format = options.delete(:format)
160
+ cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
161
+
162
+ secure = options.delete(:secure)
163
+ ssl_detected = options.delete(:ssl_detected)
164
+ secure = ssl_detected || Cloudinary.config.secure if secure.nil?
165
+ private_cdn = config_option_consume(options, :private_cdn)
166
+ secure_distribution = config_option_consume(options, :secure_distribution)
167
+ cname = config_option_consume(options, :cname)
168
+ shorten = config_option_consume(options, :shorten)
169
+ force_remote = options.delete(:force_remote)
170
+ cdn_subdomain = config_option_consume(options, :cdn_subdomain)
171
+ secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
172
+ sign_url = config_option_consume(options, :sign_url)
173
+ secret = config_option_consume(options, :api_secret)
174
+ sign_version = config_option_consume(options, :sign_version) # Deprecated behavior
175
+ url_suffix = options.delete(:url_suffix)
176
+ use_root_path = config_option_consume(options, :use_root_path)
177
+
178
+ raise(CloudinaryException, "URL Suffix only supported in private CDN") if url_suffix.present? and not private_cdn
179
+
180
+ original_source = source
181
+ return original_source if source.blank?
182
+ if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
183
+ resource_type ||= source.resource_type
184
+ type ||= source.storage_type
185
+ source = format.blank? ? source.filename : source.full_public_id
186
+ end
187
+ resource_type ||= "image"
188
+ source = source.to_s
189
+ if !force_remote
190
+ return original_source if (type.nil? || type == :asset) && source.match(%r(^https?:/)i)
191
+ if source.start_with?("/")
192
+ if source.start_with?("/images/")
193
+ source = source.sub(%r(/images/), '')
194
+ else
195
+ return original_source
196
+ end
197
+ end
198
+ @metadata ||= defined?(Cloudinary::Static) ? Cloudinary::Static.metadata : {}
199
+ if type == :asset && @metadata["images/#{source}"]
200
+ return original_source if !Cloudinary.config.static_image_support
201
+ source = @metadata["images/#{source}"]["public_id"]
202
+ source += File.extname(original_source) if !format
203
+ elsif type == :asset
204
+ return original_source # requested asset, but no metadata - probably local file. return.
205
+ end
206
+ end
207
+
208
+ resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
209
+ source, source_to_sign = finalize_source(source, format, url_suffix)
210
+
211
+ version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
212
+ version &&= "v#{version}"
213
+
214
+ transformation = transformation.gsub(%r(([^:])//), '\1/')
215
+ if sign_url
216
+ to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
217
+ signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
218
+ end
219
+
220
+ prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
221
+ source = [prefix, resource_type, type, signature, transformation, version, source].reject(&:blank?).join("/")
222
+ end
223
+
224
+ def self.finalize_source(source, format, url_suffix)
225
+ source = source.gsub(%r(([^:])//), '\1/')
226
+ if source.match(%r(^https?:/)i)
227
+ source = smart_escape(source)
228
+ source_to_sign = source
229
+ else
230
+ source = smart_escape(URI.decode(source))
231
+ source_to_sign = source
232
+ unless url_suffix.blank?
233
+ raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
234
+ source = "#{source}/#{url_suffix}"
235
+ end
236
+ if !format.blank?
237
+ source = "#{source}.#{format}"
238
+ source_to_sign = "#{source_to_sign}.#{format}"
239
+ end
240
+ end
241
+ [source, source_to_sign]
242
+ end
243
+
244
+ def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
245
+ type ||= :upload
246
+ if !url_suffix.blank?
247
+ if resource_type.to_s == "image" && type.to_s == "upload"
248
+ resource_type = "images"
249
+ type = nil
250
+ elsif resource_type.to_s == "raw" && type.to_s == "upload"
251
+ resource_type = "files"
252
+ type = nil
253
+ else
254
+ raise(CloudinaryException, "URL Suffix only supported for image/upload and raw/upload")
255
+ end
256
+ end
257
+ if use_root_path
258
+ if (resource_type.to_s == "image" && type.to_s == "upload") || (resource_type.to_s == "images" && type.blank?)
259
+ resource_type = nil
260
+ type = nil
261
+ else
262
+ raise(CloudinaryException, "Root path only supported for image/upload")
263
+ end
264
+ end
265
+ if shorten && resource_type.to_s == "image" && type.to_s == "upload"
266
+ resource_type = "iu"
267
+ type = nil
268
+ end
269
+ [resource_type, type]
270
+ end
271
+
272
+ # cdn_subdomain and secure_cdn_subdomain
273
+ # 1) Customers in shared distribution (e.g. res.cloudinary.com)
274
+ # if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
275
+ # 2) Customers with private cdn
276
+ # if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
277
+ # if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
278
+ # 3) Customers with cname
279
+ # if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
280
+ def self.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
281
+ return "/res#{cloud_name}" if cloud_name.start_with?("/") # For development
282
+
283
+ shared_domain = !private_cdn
284
+
285
+ if secure
286
+ if secure_distribution.nil? || secure_distribution == Cloudinary::OLD_AKAMAI_SHARED_CDN
287
+ secure_distribution = private_cdn ? "#{cloud_name}-res.cloudinary.com" : Cloudinary::SHARED_CDN
288
+ end
289
+ shared_domain ||= secure_distribution == Cloudinary::SHARED_CDN
290
+ secure_cdn_subdomain = cdn_subdomain if secure_cdn_subdomain.nil? && shared_domain
291
+
292
+ if secure_cdn_subdomain
293
+ secure_distribution = secure_distribution.gsub('res.cloudinary.com', "res-#{(Zlib::crc32(source) % 5) + 1}.cloudinary.com")
294
+ end
295
+
296
+ prefix = "https://#{secure_distribution}"
297
+ elsif cname
298
+ subdomain = cdn_subdomain ? "a#{(Zlib::crc32(source) % 5) + 1}." : ""
299
+ prefix = "http://#{subdomain}#{cname}"
300
+ else
301
+ host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
302
+ prefix = "http://#{host}"
303
+ end
304
+ prefix += "/#{cloud_name}" if shared_domain
305
+
306
+ prefix
307
+ end
308
+
309
+ def self.cloudinary_api_url(action = 'upload', options = {})
310
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
311
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
312
+ resource_type = options[:resource_type] || "image"
313
+ return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
314
+ end
315
+
316
+ def self.sign_request(params, options={})
317
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
318
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
319
+ params = params.reject{|k, v| self.safe_blank?(v)}
320
+ params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
321
+ params[:api_key] = api_key
322
+ params
323
+ end
324
+
325
+ def self.private_download_url(public_id, format, options = {})
326
+ cloudinary_params = sign_request({
327
+ :timestamp=>Time.now.to_i,
328
+ :public_id=>public_id,
329
+ :format=>format,
330
+ :type=>options[:type],
331
+ :attachment=>options[:attachment],
332
+ :expires_at=>options[:expires_at] && options[:expires_at].to_i
333
+ }, options)
334
+
335
+ return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
336
+ end
337
+
338
+ def self.zip_download_url(tag, options = {})
339
+ cloudinary_params = sign_request({:timestamp=>Time.now.to_i, :tag=>tag, :transformation=>generate_transformation_string(options)}, options)
340
+ return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
341
+ end
342
+
343
+ def self.signed_download_url(public_id, options = {})
344
+ aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path || raise(CloudinaryException, "Must supply aws_private_key_path")
345
+ aws_key_pair_id = options[:aws_key_pair_id] || Cloudinary.config.aws_key_pair_id || raise(CloudinaryException, "Must supply aws_key_pair_id")
346
+ authenticated_distribution = options[:authenticated_distribution] || Cloudinary.config.authenticated_distribution || raise(CloudinaryException, "Must supply authenticated_distribution")
347
+ @signers ||= Hash.new{|h,k| path, id = k; h[k] = AwsCfSigner.new(path, id)}
348
+ signer = @signers[[aws_private_key_path, aws_key_pair_id]]
349
+ url = Cloudinary::Utils.unsigned_download_url(public_id, {:type=>:authenticated}.merge(options).merge(:secure=>true, :secure_distribution=>authenticated_distribution, :private_cdn=>true))
350
+ expires_at = options[:expires_at] || (Time.now+3600)
351
+ signer.sign(url, :ending => expires_at)
352
+ end
353
+
354
+ def self.cloudinary_url(public_id, options = {})
355
+ if options[:type].to_s == 'authenticated' && !options[:sign_url]
356
+ result = signed_download_url(public_id, options)
357
+ else
358
+ result = unsigned_download_url(public_id, options)
359
+ end
360
+ return result
361
+ end
362
+
363
+ def self.asset_file_name(path)
364
+ data = Cloudinary.app_root.join(path).read(:mode=>"rb")
365
+ ext = path.extname
366
+ md5 = Digest::MD5.hexdigest(data)
367
+ public_id = "#{path.basename(ext)}-#{md5}"
368
+ "#{public_id}#{ext}"
369
+ end
370
+
371
+ # Based on CGI::unescape. In addition does not escape / :
372
+ def self.smart_escape(string)
373
+ string.gsub(/([^a-zA-Z0-9_.\-\/:]+)/) do
374
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
375
+ end
376
+ end
377
+
378
+ def self.random_public_id
379
+ sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
380
+ sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
381
+ end
382
+
383
+ def self.signed_preloaded_image(result)
384
+ "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
385
+ end
386
+
387
+ @@json_decode = false
388
+ def self.json_decode(str)
389
+ if !@@json_decode
390
+ @@json_decode = true
391
+ begin
392
+ require 'json'
393
+ rescue LoadError
394
+ begin
395
+ require 'active_support/json'
396
+ rescue LoadError
397
+ raise LoadError, "Please add the json gem or active_support to your Gemfile"
398
+ end
399
+ end
400
+ end
401
+ defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
402
+ end
403
+
404
+ def self.build_array(array)
405
+ case array
406
+ when Array then array
407
+ when nil then []
408
+ else [array]
409
+ end
410
+ end
411
+
412
+ def self.encode_hash(hash)
413
+ case hash
414
+ when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
415
+ when nil then ""
416
+ else hash
417
+ end
418
+ end
419
+
420
+ def self.encode_double_array(array)
421
+ array = build_array(array)
422
+ if array.length > 0 && array[0].is_a?(Array)
423
+ return array.map{|a| build_array(a).join(",")}.join("|")
424
+ else
425
+ return array.join(",")
426
+ end
427
+ end
428
+
429
+ IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
430
+
431
+ def self.supported_image_format?(format)
432
+ format = format.to_s.downcase
433
+ extension = format =~ /\./ ? format.split('.').last : format
434
+ IMAGE_FORMATS.include?(extension)
435
+ end
436
+
437
+ def self.resource_type_for_format(format)
438
+ self.supported_image_format?(format) ? 'image' : 'raw'
439
+ end
440
+
441
+ def self.config_option_consume(options, option_name, default_value = nil)
442
+ return options.delete(option_name) if options.include?(option_name)
443
+ return Cloudinary.config.send(option_name) || default_value
444
+ end
445
+
446
+ def self.as_bool(value)
447
+ case value
448
+ when nil then nil
449
+ when String then value.downcase == "true" || value == "1"
450
+ when TrueClass then true
451
+ when FalseClass then false
452
+ when Fixnum then value != 0
453
+ when Symbol then value == :true
454
+ else
455
+ raise "Invalid boolean value #{value} of type #{value.class}"
456
+ end
457
+ end
458
+
459
+ def self.as_safe_bool(value)
460
+ case as_bool(value)
461
+ when nil then nil
462
+ when TrueClass then 1
463
+ when FalseClass then 0
464
+ end
465
+ end
466
+
467
+ def self.safe_blank?(value)
468
+ value.nil? || value == "" || value == []
469
+ end
470
+
471
+ private
472
+ def self.number_pattern
473
+ "([0-9]*)\\.([0-9]+)|([0-9]+)"
474
+ end
475
+
476
+ def self.offset_any_pattern
477
+ "(#{number_pattern})([%pP])?"
478
+ end
479
+
480
+ def self.offset_any_pattern_re
481
+ /((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)\.\.((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)/
482
+ end
483
+
484
+ # Split a range into the start and end values
485
+ def self.split_range(range) # :nodoc:
486
+ case range
487
+ when Range
488
+ [range.first, range.last]
489
+ when String
490
+ range.split ".." if offset_any_pattern_re =~ range
491
+ when Array
492
+ [range.first, range.last]
493
+ else
494
+ nil
495
+ end
496
+ end
497
+
498
+ # Normalize an offset value
499
+ # @param [String] value a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
500
+ # @return [Object|String] a normalized String of the input value if possible otherwise the value itself
501
+ def self.norm_range_value(value) # :nodoc:
502
+ offset = /^#{offset_any_pattern}$/.match( value.to_s)
503
+ if offset
504
+ modifier = offset[5].present? ? 'p' : ''
505
+ value = "#{offset[1]}#{modifier}"
506
+ end
507
+ value
508
+ end
509
+
510
+ # A video codec parameter can be either a String or a Hash.
511
+ #
512
+ # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
513
+ # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
514
+ # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
515
+ # or the param if a String was provided.
516
+ # Returns NIL if param is not a Hash or String
517
+ def self.process_video_params(param)
518
+ case param
519
+ when Hash
520
+ video = ""
521
+ if param.has_key? :codec
522
+ video = param[:codec]
523
+ if param.has_key? :profile
524
+ video.concat ":" + param[:profile]
525
+ if param.has_key? :level
526
+ video.concat ":" + param[:level]
527
+ end
528
+ end
529
+ end
530
+ video
531
+ when String
532
+ param
533
+ else
534
+ nil
535
+ end
536
+ end
537
+
538
+ def self.deep_symbolize_keys(object)
539
+ case object
540
+ when Hash
541
+ result = {}
542
+ object.each do |key, value|
543
+ key = key.to_sym rescue key
544
+ result[key] = deep_symbolize_keys(value)
545
+ end
546
+ result
547
+ when Array
548
+ object.map{|e| deep_symbolize_keys(e)}
549
+ else
550
+ object
551
+ end
552
+ end
553
+
554
+ end