sprockets 2.1.0 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sprockets might be problematic. Click here for more details.

@@ -126,7 +126,7 @@ module Sprockets
126
126
  @directives ||= header.lines.each_with_index.map { |line, index|
127
127
  if directive = line[DIRECTIVE_PATTERN, 1]
128
128
  name, *args = Shellwords.shellwords(directive)
129
- if respond_to?("process_#{name}_directive")
129
+ if respond_to?("process_#{name}_directive", true)
130
130
  [index + 1, name, *args]
131
131
  end
132
132
  end
@@ -260,7 +260,7 @@ module Sprockets
260
260
  root = pathname.dirname.join(path).expand_path
261
261
 
262
262
  unless (stats = stat(root)) && stats.directory?
263
- raise ArgumentError, "require_tree argument must be a directory"
263
+ raise ArgumentError, "require_directory argument must be a directory"
264
264
  end
265
265
 
266
266
  context.depend_on(root)
@@ -340,6 +340,18 @@ module Sprockets
340
340
  context.depend_on_asset(path)
341
341
  end
342
342
 
343
+ # Allows dependency to be excluded from the asset bundle.
344
+ #
345
+ # The `path` must be a valid asset and may or may not already
346
+ # be part of the bundle. Once stubbed, it is blacklisted and
347
+ # can't be brought back by any other `require`.
348
+ #
349
+ # //= stub "jquery"
350
+ #
351
+ def process_stub_directive(path)
352
+ context.stub_asset(path)
353
+ end
354
+
343
355
  # Enable Sprockets 1.x compat mode.
344
356
  #
345
357
  # Makes it possible to use the same JavaScript source
@@ -23,6 +23,10 @@ module Sprockets
23
23
  self.logger = Logger.new($stderr)
24
24
  self.logger.level = Logger::FATAL
25
25
 
26
+ if respond_to?(:default_external_encoding)
27
+ self.default_external_encoding = Encoding::UTF_8
28
+ end
29
+
26
30
  # Create a safe `Context` subclass to mutate
27
31
  @context_class = Class.new(Context)
28
32
 
@@ -14,6 +14,10 @@ module Sprockets
14
14
  def initialize(environment)
15
15
  @environment = environment
16
16
 
17
+ if environment.respond_to?(:default_external_encoding)
18
+ @default_external_encoding = environment.default_external_encoding
19
+ end
20
+
17
21
  # Copy environment attributes
18
22
  @logger = environment.logger
19
23
  @context_class = environment.context_class
@@ -0,0 +1,203 @@
1
+ require 'multi_json'
2
+ require 'time'
3
+
4
+ module Sprockets
5
+ # The Manifest logs the contents of assets compiled to a single
6
+ # directory. It records basic attributes about the asset for fast
7
+ # lookup without having to compile. A pointer from each logical path
8
+ # indicates with fingerprinted asset is the current one.
9
+ #
10
+ # The JSON is part of the public API and should be considered
11
+ # stable. This should make it easy to read from other programming
12
+ # languages and processes that don't have sprockets loaded. See
13
+ # `#assets` and `#files` for more infomation about the structure.
14
+ class Manifest
15
+ attr_reader :environment, :path, :dir
16
+
17
+ # Create new Manifest associated with an `environment`. `path` is
18
+ # a full path to the manifest json file. The file may or may not
19
+ # already exist. The dirname of the `path` will be used to write
20
+ # compiled assets to. Otherwise, if the path is a directory, the
21
+ # filename will default to "manifest.json" in that directory.
22
+ #
23
+ # Manifest.new(environment, "./public/assets/manifest.json")
24
+ #
25
+ def initialize(environment, path)
26
+ @environment = environment
27
+
28
+ if File.extname(path) == ""
29
+ @dir = File.expand_path(path)
30
+ @path = File.join(@dir, 'manifest.json')
31
+ else
32
+ @path = File.expand_path(path)
33
+ @dir = File.dirname(path)
34
+ end
35
+
36
+ data = nil
37
+
38
+ begin
39
+ if File.exist?(@path)
40
+ data = MultiJson.decode(File.read(@path))
41
+ end
42
+ rescue MultiJson::DecodeError => e
43
+ logger.error "#{@path} is invalid: #{e.class} #{e.message}"
44
+ end
45
+
46
+ @data = data.is_a?(Hash) ? data : {}
47
+ end
48
+
49
+ # Returns internal assets mapping. Keys are logical paths which
50
+ # map to the latest fingerprinted filename.
51
+ #
52
+ # Logical path (String): Fingerprint path (String)
53
+ #
54
+ # { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",
55
+ # "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }
56
+ #
57
+ def assets
58
+ @data['assets'] ||= {}
59
+ end
60
+
61
+ # Returns internal file directory listing. Keys are filenames
62
+ # which map to an attributes array.
63
+ #
64
+ # Fingerprint path (String):
65
+ # logical_path: Logical path (String)
66
+ # mtime: ISO8601 mtime (String)
67
+ # digest: Base64 hex digest (String)
68
+ #
69
+ # { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =>
70
+ # { 'logical_path' => "application.js",
71
+ # 'mtime' => "2011-12-13T21:47:08-06:00",
72
+ # 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }
73
+ #
74
+ def files
75
+ @data['files'] ||= {}
76
+ end
77
+
78
+ # Compile and write asset to directory. The asset is written to a
79
+ # fingerprinted filename like
80
+ # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
81
+ # also inserted into the manifest file.
82
+ #
83
+ # compile("application.js")
84
+ #
85
+ def compile(*args)
86
+ paths = environment.each_logical_path(*args).to_a +
87
+ args.flatten.select { |fn| Pathname.new(fn).absolute? }
88
+
89
+ paths.each do |path|
90
+ if asset = find_asset(path)
91
+ files[asset.digest_path] = {
92
+ 'logical_path' => asset.logical_path,
93
+ 'mtime' => asset.mtime.iso8601,
94
+ 'digest' => asset.digest
95
+ }
96
+ assets[asset.logical_path] = asset.digest_path
97
+
98
+ target = File.join(dir, asset.digest_path)
99
+
100
+ if File.exist?(target)
101
+ logger.debug "Skipping #{target}, already exists"
102
+ else
103
+ logger.info "Writing #{target}"
104
+ asset.write_to target
105
+ end
106
+
107
+ save
108
+ asset
109
+ end
110
+ end
111
+ end
112
+
113
+ # Removes file from directory and from manifest. `filename` must
114
+ # be the name with any directory path.
115
+ #
116
+ # manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
117
+ #
118
+ def remove(filename)
119
+ path = File.join(dir, filename)
120
+ logical_path = files[filename]['logical_path']
121
+
122
+ if assets[logical_path] == filename
123
+ assets.delete(logical_path)
124
+ end
125
+
126
+ files.delete(filename)
127
+ FileUtils.rm(path) if File.exist?(path)
128
+
129
+ save
130
+
131
+ logger.warn "Removed #{filename}"
132
+
133
+ nil
134
+ end
135
+
136
+ # Cleanup old assets in the compile directory. By default it will
137
+ # keep the latest version plus 2 backups.
138
+ def clean(keep = 2)
139
+ self.assets.keys.each do |logical_path|
140
+ # Get assets sorted by ctime, newest first
141
+ assets = backups_for(logical_path)
142
+
143
+ # Keep the last N backups
144
+ assets = assets[keep..-1] || []
145
+
146
+ # Remove old assets
147
+ assets.each { |path, _| remove(path) }
148
+ end
149
+ end
150
+
151
+ # Wipe directive
152
+ def clobber
153
+ FileUtils.rm_r(@dir) if File.exist?(@dir)
154
+ logger.warn "Removed #{@dir}"
155
+ nil
156
+ end
157
+
158
+ protected
159
+ # Finds all the backup assets for a logical path. The latest
160
+ # version is always excluded. The return array is sorted by the
161
+ # assets mtime in descending order (Newest to oldest).
162
+ def backups_for(logical_path)
163
+ files.select { |filename, attrs|
164
+ # Matching logical paths
165
+ attrs['logical_path'] == logical_path &&
166
+ # Excluding whatever asset is the current
167
+ assets[logical_path] != filename
168
+ }.sort_by { |filename, attrs|
169
+ # Sort by timestamp
170
+ Time.parse(attrs['mtime'])
171
+ }.reverse
172
+ end
173
+
174
+ # Basic wrapper around Environment#find_asset. Logs compile time.
175
+ def find_asset(logical_path)
176
+ asset = nil
177
+ ms = benchmark do
178
+ asset = environment.find_asset(logical_path)
179
+ end
180
+ logger.warn "Compiled #{logical_path} (#{ms}ms)"
181
+ asset
182
+ end
183
+
184
+ # Persist manfiest back to FS
185
+ def save
186
+ FileUtils.mkdir_p dir
187
+ File.open(path, 'w') do |f|
188
+ f.write MultiJson.encode(@data)
189
+ end
190
+ end
191
+
192
+ private
193
+ def logger
194
+ environment.logger
195
+ end
196
+
197
+ def benchmark
198
+ start_time = Time.now.to_f
199
+ yield
200
+ ((Time.now.to_f - start_time) * 1000).to_i
201
+ end
202
+ end
203
+ end
@@ -30,6 +30,16 @@ module Sprockets
30
30
  ext = Sprockets::Utils.normalize_extension(ext)
31
31
  @mime_types[ext] = mime_type
32
32
  end
33
+
34
+ if defined? Encoding
35
+ # Returns the correct encoding for a given mime type, while falling
36
+ # back on the default external encoding, if it exists.
37
+ def encoding_for_mime_type(type)
38
+ encoding = Encoding::BINARY if type =~ %r{^(image|audio|video)/}
39
+ encoding ||= default_external_encoding if respond_to?(:default_external_encoding)
40
+ encoding
41
+ end
42
+ end
33
43
  end
34
44
 
35
45
  # Extend Sprockets module to provide global registry
@@ -94,27 +94,31 @@ module Sprockets
94
94
 
95
95
  private
96
96
  def build_required_assets(environment, context)
97
- @required_assets = []
98
- required_assets_cache = {}
97
+ @required_assets = resolve_dependencies(environment, context._required_paths + [pathname.to_s]) -
98
+ resolve_dependencies(environment, context._stubbed_assets.to_a)
99
+ end
100
+
101
+ def resolve_dependencies(environment, paths)
102
+ assets = []
103
+ cache = {}
99
104
 
100
- (context._required_paths + [pathname.to_s]).each do |path|
105
+ paths.each do |path|
101
106
  if path == self.pathname.to_s
102
- unless required_assets_cache[self]
103
- required_assets_cache[self] = true
104
- @required_assets << self
107
+ unless cache[self]
108
+ cache[self] = true
109
+ assets << self
105
110
  end
106
111
  elsif asset = environment.find_asset(path, :bundle => false)
107
112
  asset.required_assets.each do |asset_dependency|
108
- unless required_assets_cache[asset_dependency]
109
- required_assets_cache[asset_dependency] = true
110
- @required_assets << asset_dependency
113
+ unless cache[asset_dependency]
114
+ cache[asset_dependency] = true
115
+ assets << asset_dependency
111
116
  end
112
117
  end
113
118
  end
114
119
  end
115
120
 
116
- required_assets_cache.clear
117
- required_assets_cache = nil
121
+ assets
118
122
  end
119
123
 
120
124
  def build_dependency_paths(environment, context)
@@ -25,11 +25,6 @@ module Sprockets
25
25
 
26
26
  msg = "Served asset #{env['PATH_INFO']} -"
27
27
 
28
- # URLs containing a `".."` are rejected for security reasons.
29
- if forbidden_request?(env)
30
- return forbidden_response
31
- end
32
-
33
28
  # Mark session as "skipped" so no `Set-Cookie` header is set
34
29
  env['rack.session.options'] ||= {}
35
30
  env['rack.session.options'][:defer] = true
@@ -43,6 +38,11 @@ module Sprockets
43
38
  path = path.sub("-#{fingerprint}", '')
44
39
  end
45
40
 
41
+ # URLs containing a `".."` are rejected for security reasons.
42
+ if forbidden_request?(path)
43
+ return forbidden_response
44
+ end
45
+
46
46
  # Look up the asset.
47
47
  asset = find_asset(path, :bundle => !body_only?(env))
48
48
 
@@ -53,9 +53,8 @@ module Sprockets
53
53
  # Return a 404 Not Found
54
54
  not_found_response
55
55
 
56
- # Check request headers `HTTP_IF_MODIFIED_SINCE` and
57
- # `HTTP_IF_NONE_MATCH` against the assets mtime and digest
58
- elsif not_modified?(asset, env) || etag_match?(asset, env)
56
+ # Check request headers `HTTP_IF_NONE_MATCH` against the asset digest
57
+ elsif etag_match?(asset, env)
59
58
  logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
60
59
 
61
60
  # Return a 304 Not Modified
@@ -86,12 +85,12 @@ module Sprockets
86
85
  end
87
86
 
88
87
  private
89
- def forbidden_request?(env)
88
+ def forbidden_request?(path)
90
89
  # Prevent access to files elsewhere on the file system
91
90
  #
92
91
  # http://example.org/assets/../../../etc/passwd
93
92
  #
94
- env["PATH_INFO"].include?("..")
93
+ path.include?("..") || Pathname.new(path).absolute?
95
94
  end
96
95
 
97
96
  # Returns a 403 Forbidden response tuple
@@ -174,12 +173,6 @@ module Sprockets
174
173
  gsub('/', '\\\\002f ')
175
174
  end
176
175
 
177
- # Compare the requests `HTTP_IF_MODIFIED_SINCE` against the
178
- # assets mtime
179
- def not_modified?(asset, env)
180
- env["HTTP_IF_MODIFIED_SINCE"] == asset.mtime.httpdate
181
- end
182
-
183
176
  # Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
184
177
  def etag_match?(asset, env)
185
178
  env["HTTP_IF_NONE_MATCH"] == etag(asset)
@@ -229,7 +222,7 @@ module Sprockets
229
222
  # # => "0aa2105d29558f3eb790d411d7d8fb66"
230
223
  #
231
224
  def path_fingerprint(path)
232
- path[/-([0-9a-f]{7,40})\.[^.]+$/, 1]
225
+ path[/-([0-9a-f]{7,40})\.[^.]+\z/, 1]
233
226
  end
234
227
 
235
228
  # URI.unescape is deprecated on 1.9. We need to use URI::Parser
@@ -23,6 +23,8 @@ module Sprockets
23
23
  # Gzip contents if filename has '.gz'
24
24
  options[:compress] ||= File.extname(filename) == '.gz'
25
25
 
26
+ FileUtils.mkdir_p File.dirname(filename)
27
+
26
28
  if options[:compress]
27
29
  # Open file and run it through `Zlib`
28
30
  pathname.open('rb') do |rd|
@@ -8,19 +8,21 @@ module Sprockets
8
8
  # encoding and we want to avoid syntax errors in other interpreters.
9
9
  UTF8_BOM_PATTERN = Regexp.new("\\A\uFEFF".encode('utf-8'))
10
10
 
11
- def self.read_unicode(pathname)
12
- pathname.read.tap do |data|
13
- # Eager validate the file's encoding. In most cases we
14
- # expect it to be UTF-8 unless `default_external` is set to
15
- # something else. An error is usually raised if the file is
16
- # saved as UTF-16 when we expected UTF-8.
17
- if !data.valid_encoding?
18
- raise EncodingError, "#{pathname} has a invalid " +
19
- "#{data.encoding} byte sequence"
11
+ def self.read_unicode(pathname, external_encoding = Encoding.default_external)
12
+ pathname.open("r:#{external_encoding}") do |f|
13
+ f.read.tap do |data|
14
+ # Eager validate the file's encoding. In most cases we
15
+ # expect it to be UTF-8 unless `default_external` is set to
16
+ # something else. An error is usually raised if the file is
17
+ # saved as UTF-16 when we expected UTF-8.
18
+ if !data.valid_encoding?
19
+ raise EncodingError, "#{pathname} has a invalid " +
20
+ "#{data.encoding} byte sequence"
20
21
 
21
- # If the file is UTF-8 and theres a BOM, strip it for safe concatenation.
22
- elsif data.encoding.name == "UTF-8" && data =~ UTF8_BOM_PATTERN
23
- data.sub!(UTF8_BOM_PATTERN, "")
22
+ # If the file is UTF-8 and theres a BOM, strip it for safe concatenation.
23
+ elsif data.encoding.name == "UTF-8" && data =~ UTF8_BOM_PATTERN
24
+ data.sub!(UTF8_BOM_PATTERN, "")
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -1,3 +1,3 @@
1
1
  module Sprockets
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.3"
3
3
  end