mustache 1.1.1 → 1.1.3

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,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfcec948b9e5fbd73d9c72e3ade21724d3fd73a1d2ee2ea58ab26bf7e1a341ba
4
- data.tar.gz: 1a5d4d062ad29d4a4057a6489f6779d3e61bb7132ecf56fc1ab97a48bfcb926d
3
+ metadata.gz: bf066480926aaca95b8ebbb4e45de15a11cae753e407be03a8059bee4deb9837
4
+ data.tar.gz: a334278f764909eb095bed8bf31132af6c61d6dffadbf5688cd745e85517a7a6
5
5
  SHA512:
6
- metadata.gz: afd9888e324611b31fb9a6ac809393b2c42261befce363ab691d68719edb2687dda2129ba64a12367bd0f39d31b1fda861b209345f2abfc06debf67aa0e3ab99
7
- data.tar.gz: c225773518109b0eeeecaf89d39faaec30b951361b10c16944f4e016aec6597fc41e75237920dc8c44e00956a00edfcf1b72f2e914ea4dc3f8b296f2436977dc
6
+ metadata.gz: 571c5ff585457d1073fa4de0d506073b55a7c3409967d39a488b71fe38b03e7074312ad5e9e379d0f654043dc8be7ffdba4e1cb3b6df4ba6e884920885064ecd
7
+ data.tar.gz: cb3b39d80301b99e13e4cf412e5da3065ecf5215d7b7bece535c032e6cec5c452a0d44f43363d4d35a06a2267e67945325c177ae8631f8b19bac8f344fad2768
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Mustache
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/mustache.svg)](http://badge.fury.io/rb/mustache)
4
- [![Build Status](https://travis-ci.org/mustache/mustache.svg?branch=master)](https://travis-ci.org/mustache/mustache)
4
+ [![Test](https://github.com/mustache/mustache/actions/workflows/test.yml/badge.svg)](https://github.com/mustache/mustache/actions/workflows/test.yml)
5
5
 
6
6
  Inspired by [ctemplate][1] and [et][2], Mustache is a
7
7
  framework-agnostic way to render logic-free views.
@@ -166,8 +166,9 @@ follows the classic Ruby naming convention.
166
166
 
167
167
  TemplatePartial => ./template_partial.mustache
168
168
 
169
- You can set the search path using `Mustache.template_path`. It can be set on a
170
- class by class basis:
169
+ You can set the search path using `Mustache.template_path`. Search multiple
170
+ paths by delimiting them with your platform's 'path separator' (colon on Unix).
171
+ It can be set on a class by class basis:
171
172
 
172
173
  ```ruby
173
174
  class Simple < Mustache
@@ -381,9 +382,9 @@ Once you've made your great commits:
381
382
 
382
383
  ~~To join the list simply send an email to <mustache@librelist.com>. This
383
384
  will subscribe you and send you information about your subscription,
384
- including unsubscribe information.
385
+ including unsubscribe information.~~
385
386
 
386
- The archive can be found at <http://librelist.com/browser/mustache/>.~~
387
+ ~~The archive can be found at <http://librelist.com/browser/mustache/>.~~
387
388
 
388
389
  The mailing list hasn't been updated in quite a while, please join us on Gitter
389
390
  or IRC:
data/Rakefile CHANGED
@@ -35,7 +35,7 @@ if command? :ronn
35
35
 
36
36
  desc "Build the manual"
37
37
  task "man:build" do
38
- sh "ronn -br5 --organization=DEFUNKT --manual='Mustache Manual' man/*.ronn"
38
+ sh "ronn-ng -br5 --organization=DEFUNKT --manual='Mustache Manual' man/*.ron"
39
39
  end
40
40
  end
41
41
 
data/bin/mustache CHANGED
@@ -11,7 +11,7 @@ class Mustache
11
11
  # Return a structure describing the options.
12
12
  def self.parse_options(args)
13
13
  opts = OptionParser.new do |opts|
14
- opts.banner = "Usage: mustache [-c] [-t] [-e] [-r library] FILE ..."
14
+ opts.banner = "Usage: mustache [-c] [-t] [-l level] [-e] [-r library] FILE ..."
15
15
 
16
16
  opts.separator " "
17
17
 
@@ -42,6 +42,11 @@ class Mustache
42
42
  exit
43
43
  end
44
44
 
45
+ opts.on("-l", "--level",
46
+ "Provide a context access security level.") do |level|
47
+ Mustache.context_access_security_level = level.to_i
48
+ end
49
+
45
50
  opts.on("-e", "--error",
46
51
  "Raise an error on context misses.") do |e|
47
52
  Mustache.raise_on_context_miss = true
@@ -136,14 +136,35 @@ class Mustache
136
136
  # @return [Object] The value of key in object if it is found, and default otherwise.
137
137
  #
138
138
  def find(obj, key, default = nil)
139
+
140
+ if mustache_in_stack.context_access_security_level == 5 and !obj.instance_of?(Hash)
141
+ # on level 5, object must be exactly Hash
142
+ return context_level_violation("Detected key access attempt for a non-Hash object using '#{key}'", default)
143
+ end
144
+
139
145
  return find_in_hash(obj.to_hash, key, default) if obj.respond_to?(:to_hash)
140
146
 
147
+ if mustache_in_stack.context_access_security_level >= 4
148
+ # on levels 4 and 5, method access is not allowed
149
+ return context_level_violation("Detected method access attempt using '#{key}'", default)
150
+ end
151
+
141
152
  unless obj.respond_to?(key)
142
153
  # no match for the key, but it may include a hyphen, so try again replacing hyphens with underscores.
143
154
  key = key.to_s.tr('-', '_')
144
155
  return default unless obj.respond_to?(key)
145
156
  end
146
157
 
158
+ if mustache_in_stack.context_access_security_level >= 3 && !obj.class.instance_methods(false).include?(key.to_sym)
159
+ # on level 3, inherited methods are blocked
160
+ return context_level_violation("Detected inherited method '#{key}'", default)
161
+ end
162
+
163
+ if mustache_in_stack.context_access_security_level >= 2 && MethodBlacklist.include?(key.to_s)
164
+ # on levels 2 and 3, reflection-based methods are blacklisted
165
+ return context_level_violation("Detected blacklisted reflection-related key '#{key}'", default)
166
+ end
167
+
147
168
  meth = obj.method(key) rescue proc { obj.send(key) }
148
169
  meth.arity == 1 ? meth.to_proc : meth.call
149
170
  end
@@ -11,4 +11,45 @@ class Mustache
11
11
  #
12
12
  class ContextMiss < RuntimeError; end
13
13
 
14
+ # A ContextLevelViolation is raised whenever accessing a key in the context is
15
+ # not allowed by the selected context access security level
16
+ #
17
+ # For example, if your context access security level is set to 3 but
18
+ # your template tries to access an inherited method this exception will be raised.
19
+ #
20
+ # By default it is only raised for blacklisted keys in objects.
21
+ # See Mustache.context_access_security_level.
22
+ #
23
+ class ContextLevelViolation < ContextMiss; end
24
+
25
+ class Context
26
+ # Raises ContextLevelViolation or returns default depending on raise_on_context_miss
27
+ #
28
+ # @param [String] message The error message if raise_on_context_miss is set
29
+ # @param [Object] default An optional default value, to return if the key is not found.
30
+ #
31
+ # @return [Object] The default value if errors are not raised
32
+ #
33
+ def context_level_violation(message, default = nil)
34
+ return default unless mustache_in_stack.raise_on_context_miss?
35
+ raise ContextLevelViolation.new(message)
36
+ end
37
+ end
38
+
39
+
40
+ MethodBlacklist = ["allocate", "attached_object", "superclass", "subclasses", "new", "autoload?", "autoload",
41
+ "included_modules", "include?", "set_temporary_name", "ancestors", "attr", "attr_reader", "attr_writer",
42
+ "attr_accessor", "public_instance_method", "instance_methods", "public_instance_methods",
43
+ "protected_instance_methods", "private_instance_methods", "undefined_instance_methods", "freeze", "const_get",
44
+ "constants", "const_missing", "const_defined?", "const_set", "const_source_location", "remove_class_variable",
45
+ "class_variable_get", "class_variables", "private_constant", "class_variable_set", "class_variable_defined?",
46
+ "public_constant", "include", "deprecate_constant", "singleton_class?", "prepend", "refinements", "define_method",
47
+ "module_exec", "class_exec", "module_eval", "class_eval", "remove_method", "undef_method", "alias_method",
48
+ "method_defined?", "public_method_defined?", "private_method_defined?", "protected_method_defined?",
49
+ "public_class_method", "private_class_method", "instance_method", "singleton_class", "dup", "itself", "methods",
50
+ "singleton_methods", "protected_methods", "private_methods", "public_methods", "instance_variables",
51
+ "instance_variable_get", "instance_variable_set", "instance_variable_defined?", "remove_instance_variable",
52
+ "instance_of?", "kind_of?", "is_a?", "display", "public_send", "extend", "clone", "class", "frozen?", "tap",
53
+ "then", "yield_self", "respond_to?", "method", "public_method", "singleton_method", "define_singleton_method",
54
+ "hash", "object_id", "send", "enum_for", "__send__", "instance_eval", "instance_exec", "__id__"]
14
55
  end
@@ -88,7 +88,7 @@ class Mustache
88
88
  def compile!(exp)
89
89
  case exp.first
90
90
  when :multi
91
- exp[1..-1].reduce("") { |sum, e| sum << compile!(e) }
91
+ exp[1..-1].reduce("".dup) { |sum, e| sum << compile!(e) }
92
92
  when :static
93
93
  str(exp[1])
94
94
  when :mustache
@@ -151,7 +151,7 @@ class Mustache
151
151
  # what's inside.
152
152
  ev(<<-compiled)
153
153
  v = #{compile!(name)}
154
- if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
154
+ if v.nil? || v == false || (v.class != String && v.respond_to?(:empty?) && v.empty?)
155
155
  #{code}
156
156
  end
157
157
  compiled
@@ -78,7 +78,7 @@ EOF
78
78
  SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ].map(&:freeze)
79
79
 
80
80
  # The content allowed in a tag name.
81
- ALLOWED_CONTENT = /(\w|[?!\/.-])*/
81
+ ALLOWED_CONTENT = /(\w|[?!\/.@-])*/
82
82
 
83
83
  # These types of tags allow any content,
84
84
  # the rest only allow ALLOWED_CONTENT.
@@ -9,6 +9,7 @@ class Mustache
9
9
  @template_name = nil
10
10
  @template_file = nil
11
11
  @raise_on_context_miss = nil
12
+ @context_access_security_level = 2
12
13
  end
13
14
 
14
15
  def self.initialize_settings
@@ -18,6 +19,7 @@ class Mustache
18
19
  @template_name = nil
19
20
  @template_file = nil
20
21
  @raise_on_context_miss = nil
22
+ @context_access_security_level = 2
21
23
  end
22
24
 
23
25
  initialize_settings
@@ -31,38 +33,43 @@ class Mustache
31
33
  #
32
34
 
33
35
  # The template path informs your Mustache view where to look for its
34
- # corresponding template. By default it's the current directory (".")
36
+ # corresponding template. It is an array of paths, by default containing
37
+ # one entry: the current directory (".")
38
+ # When setting up from a string, use path delimiters to create a search path
39
+ # When the template_file is requested, a search is done to find the file in the
40
+ # path, this is then stored as the name.
35
41
  #
36
42
  # A class named Stat with a template_path of "app/templates" will look
37
43
  # for "app/templates/stat.mustache"
38
44
 
45
+ def self.setup_path path
46
+ path = path.split(File::PATH_SEPARATOR) if path.is_a? String
47
+ path.map{|p| File.expand_path(p)}
48
+ end
49
+
39
50
  def self.template_path
40
- @template_path ||= inheritable_config_for :template_path, '.'
51
+ @template_path ||= setup_path(inheritable_config_for(:template_path, '.'))
41
52
  end
42
53
 
43
54
  def self.template_path=(path)
44
- @template_path = File.expand_path(path)
55
+ @template_path = setup_path(path)
45
56
  @template = nil
46
57
  end
47
58
 
48
59
  def template_path
49
60
  @template_path ||= self.class.template_path
50
61
  end
62
+
51
63
  alias_method :path, :template_path
52
64
 
53
65
  def template_path=(path)
54
- @template_path = File.expand_path(path)
66
+ @template_path = self.class.setup_path(path)
55
67
  @template = nil
56
68
  end
57
69
 
58
- # Alias for `template_path`
59
- def self.path
60
- template_path
61
- end
62
-
63
- # Alias for `template_path`
64
- def self.path=(path)
65
- self.template_path = path
70
+ class << self
71
+ alias_method :path, :template_path
72
+ alias_method :path=, :template_path=
66
73
  end
67
74
 
68
75
 
@@ -128,26 +135,25 @@ class Mustache
128
135
  # Template File
129
136
  #
130
137
 
131
- # The template file is the absolute path of the file Mustache will
132
- # use as its template. By default it's ./class_name.mustache
138
+ # The template file is the absolute path of the file Mustache will use as its template.
139
+ # By default it's ./class_name.mustache
140
+ #
133
141
 
134
142
  def self.template_file
135
- @template_file || "#{path}/#{template_name}.#{template_extension}"
143
+ @template_file || path.map{|p| "#{p}/#{template_name}.#{template_extension}" }.find{|tf| File.readable? tf}
136
144
  end
137
145
 
138
- def self.template_file=(template_file)
139
- @template_file = template_file
146
+ def self.template_file=(tf)
147
+ @template_file = tf
140
148
  @template = nil
141
149
  end
142
150
 
143
- # The template file is the absolute path of the file Mustache will
144
- # use as its template. By default it's ./class_name.mustache
145
151
  def template_file
146
- @template_file || "#{path}/#{template_name}.#{template_extension}"
152
+ @template_file || path.map{|p| "#{p}/#{template_name}.#{template_extension}" }.find{|tf| File.readable? tf}
147
153
  end
148
154
 
149
- def template_file=(template_file)
150
- @template_file = template_file
155
+ def template_file=(tf)
156
+ @template_file = tf
151
157
  @template = nil
152
158
  end
153
159
 
@@ -214,6 +220,37 @@ class Mustache
214
220
  @raise_on_context_miss = boolean
215
221
  end
216
222
 
223
+ #
224
+ # Context access security level
225
+ #
226
+
227
+ # What object methods could be accessed from the template?
228
+ # This level determines the security restrictions for method access to protect against unsafe reflection
229
+ # Possible values are integers from 1 to 5:
230
+ #
231
+ # 1 - All methods allowed (use only with trusted templates)
232
+ # 2 - Reflection-related methods are blacklisted (default)
233
+ # 3 - All inherited or reflection-related methods are prohibited
234
+ # 4 - Access allowed only for Hash elements
235
+ # 5 - Access allowed only for Hash elements, only numbers, strings, hashes and functions allowed
236
+
237
+ def self.context_access_security_level
238
+ @context_access_security_level
239
+ end
240
+
241
+ def self.context_access_security_level=(integer)
242
+ @context_access_security_level = integer
243
+ end
244
+
245
+ # Instance level version of `Mustache.context_access_security_level`
246
+ def context_access_security_level
247
+ self.class.context_access_security_level || @context_access_security_level
248
+ end
249
+
250
+ def context_access_security_level=(integer)
251
+ @context_access_security_level = integer
252
+ end
253
+
217
254
 
218
255
  #
219
256
  # View Namespace
@@ -22,7 +22,7 @@ class Mustache
22
22
  .split('::')
23
23
  .map do |part|
24
24
  part[0] = part[0].downcase
25
- part.gsub(/[A-Z]/) { |s| "_" << s.downcase }
25
+ part.gsub(/[A-Z]/) { |s| '_'.dup << s.downcase }
26
26
  end
27
27
  .join('/')
28
28
  end
@@ -1,3 +1,3 @@
1
1
  class Mustache
2
- VERSION = '1.1.1'
2
+ VERSION = '1.1.3'
3
3
  end
data/lib/mustache.rb CHANGED
@@ -187,14 +187,11 @@ class Mustache
187
187
  # reading templates from a database. It will be rendered by the
188
188
  # context, so all you need to do is return a string.
189
189
  def partial(name)
190
- path = "#{template_path}/#{name}.#{template_extension}"
190
+ partialpath = template_path.map{|p| "#{p}/#{name}.#{template_extension}" }.find{|pf| File.readable? pf}
191
191
 
192
- begin
193
- File.read(path)
194
- rescue
195
- raise if raise_on_context_miss?
196
- ""
197
- end
192
+ raise RuntimeError.new("Can't find partial #{name}") if not partialpath and raise_on_context_miss?
193
+
194
+ partialpath ? File.read(partialpath) : ""
198
195
  end
199
196
 
200
197
  # Override this to provide custom escaping.