Class: Immunio::Template
- Inherits:
-
Object
- Object
- Immunio::Template
- Defined in:
- lib/immunio/plugins/action_view.rb
Overview
Renders templates by filtering them through Immunio’s hook handlers.
Constant Summary collapse
- CHECKSUM_CACHE =
Hash.new do |cache, template_id| template = ObjectSpace._id2ref(template_id) if template.respond_to?(:source) && !template.source.nil? finalizer = Immunio::Template.finalize_template(template_id) ObjectSpace.define_finalizer(template, finalizer) cache[template_id] = Digest::SHA1.hexdigest(template.source).freeze end end
Instance Attribute Summary collapse
-
#vars ⇒ Object
Returns the value of attribute vars.
Class Method Summary collapse
- .current ⇒ Object
- .finalize_template(id) ⇒ Object
-
.generate_render_var_code(code, escape) ⇒ Object
Generate code injected in templates to wrap everything inside ‘<%= …
- .get_nonce ⇒ Object
-
.mark_and_defer_fragment_write(*args) ⇒ Object
Save fragment info to the root template only.
- .mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
- .next_var_id ⇒ Object
- .render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
- .vars ⇒ Object
Instance Method Summary collapse
- #==(other) ⇒ Object
- #compiled? ⇒ Boolean
- #get_nonce ⇒ Object
- #has_source? ⇒ Boolean
- #id ⇒ Object
-
#initialize(template) ⇒ Template
constructor
A new instance of Template.
- #is_text? ⇒ Boolean
- #load_source(context) ⇒ Object
- #mark_and_defer_fragment_write(key, content, options) ⇒ Object
- #next_template_id ⇒ Object
-
#next_var_id ⇒ Object
Generate the next var unique ID to be used in a template.
- #render(context) ⇒ Object
- #template_sha ⇒ Object
Constructor Details
#initialize(template) ⇒ Template
Returns a new instance of Template.
23 24 25 26 27 28 29 |
# File 'lib/immunio/plugins/action_view.rb', line 23 def initialize(template) @template = template @next_var_id = 0 @next_template_id = 0 @vars = {} @scheduled_fragments_writes = [] end |
Instance Attribute Details
#vars ⇒ Object
Returns the value of attribute vars.
21 22 23 |
# File 'lib/immunio/plugins/action_view.rb', line 21 def vars @vars end |
Class Method Details
.current ⇒ Object
271 272 273 |
# File 'lib/immunio/plugins/action_view.rb', line 271 def self.current rendering_stack.last end |
.finalize_template(id) ⇒ Object
17 18 19 |
# File 'lib/immunio/plugins/action_view.rb', line 17 def self.finalize_template(id) proc { CHECKSUM_CACHE.delete(id) if CHECKSUM_CACHE.has_key?(id) } end |
.generate_render_var_code(code, escape) ⇒ Object
Generate code injected in templates to wrap everything inside ‘<%= … %>`.
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/immunio/plugins/action_view.rb', line 241 def self.generate_render_var_code(code, escape) template = Template.current if template template_id = template.next_template_id handler = template.instance_variable_get(:@template).handler handler_name = if handler.is_a? Class handler.name else handler.class.name end "(__immunio_result = (#{code}); Immunio::Template.render_var(#{code.strip.inspect}, __immunio_result, #{template_id}, '#{template.template_sha}', __FILE__, __LINE__, #{escape}, #{template.is_text?}, '#{handler_name}'))" else code end end |
.get_nonce ⇒ Object
283 284 285 |
# File 'lib/immunio/plugins/action_view.rb', line 283 def self.get_nonce rendering_stack.first.get_nonce end |
.mark_and_defer_fragment_write(*args) ⇒ Object
Save fragment info to the root template only
288 289 290 |
# File 'lib/immunio/plugins/action_view.rb', line 288 def self.mark_and_defer_fragment_write(*args) rendering_stack.first.mark_and_defer_fragment_write(*args) end |
.mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/immunio/plugins/action_view.rb', line 116 def self.mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) id = Template.next_var_id nonce = Template.get_nonce # NOTE: What happens here is pretty funky to preserve the html_safe SafeBuffer behaviour in ruby. # If escaped is true we directly concatenate the content between two SafeBuffers. This will cause # escaping if content is not itself a SafeBuffer. # Otherwise we explicitly convert to a string, and convert that to a SafeBuffer to ensure that # for instance no escaping is performed on the contents of a <%== %> Erubis interpolation. rendering = if escape && !is_text # explicitly convert (w/ escapes) and mark safe things that aren't String (SafeBuffer is_a String also) # `to_s` is used to render any object passed to a template. # It is called internally when appending to ActionView::OutputBuffer. # We force rendering to get the actual string. # This has no impact if `rendered` is already a string. content = content.to_s.html_safe unless content.is_a? String # As a failsafe, just return the content if it already contains our markers. This can occur when # a helper calls render partial to generate a component of a page. Both render calls are root level # templates from our perspective. if content =~ /\{immunio-var:\d+:#{nonce}\}/ then # don't add markers. Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""} return content end "{immunio-var:#{id}:#{nonce}}".html_safe + content + "{/immunio-var:#{id}:#{nonce}}".html_safe else content = "" if content.nil? # See comment above if content =~ /\{immunio-var:\d+:#{nonce}\}/ then # don't add markers. Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""} return content.html_safe end "{immunio-var:#{id}:#{nonce}}".html_safe + content.to_s.html_safe + "{/immunio-var:#{id}:#{nonce}}".html_safe end # If we got here, the interpolation has been wrapped in our markers and we # need to record send data about it to the hook Template.vars[id.to_s] = { template_sha: template_sha, template_id: template_id.to_s, nonce: nonce, code: wrap_code(code, handler, escape: escape), file: file, line: line } rendering end |
.next_var_id ⇒ Object
275 276 277 |
# File 'lib/immunio/plugins/action_view.rb', line 275 def self.next_var_id rendering_stack.first.next_var_id end |
.render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/immunio/plugins/action_view.rb', line 259 def self.render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) if rendered.instance_variable_get("@__immunio_processed") then # Ignore buffers marked as __immunio_processed in render as these are full templates or partials return rendered elsif code =~ /yield( .*)?/ # Ignore yielded blocks inside layouts return rendered end rendered = mark_var rendered, code, template_id, template_sha, file, line, escape, is_text, handler rendered.html_safe end |
.vars ⇒ Object
279 280 281 |
# File 'lib/immunio/plugins/action_view.rb', line 279 def self.vars rendering_stack.first.vars end |
Instance Method Details
#==(other) ⇒ Object
35 36 37 |
# File 'lib/immunio/plugins/action_view.rb', line 35 def ==(other) self.class === other && id == other.id end |
#compiled? ⇒ Boolean
92 93 94 |
# File 'lib/immunio/plugins/action_view.rb', line 92 def compiled? @template.instance_variable_get :@compiled end |
#get_nonce ⇒ Object
109 110 111 112 113 114 |
# File 'lib/immunio/plugins/action_view.rb', line 109 def get_nonce # Generate a two byte CSRNG nonce to make our substitutions unpreictable # Why only 2 bytes? The nonce is per render, so the odds of guessing it are very low # and entropy is finite so we don't want to drain the random pool unnecessarily @nonce ||= SecureRandom.hex(2) end |
#has_source? ⇒ Boolean
39 40 41 |
# File 'lib/immunio/plugins/action_view.rb', line 39 def has_source? @template.respond_to?(:source) && !@template.source.nil? end |
#id ⇒ Object
31 32 33 |
# File 'lib/immunio/plugins/action_view.rb', line 31 def id (@template.respond_to?(:virtual_path) && @template.virtual_path) || (@template.respond_to?(:source) && @template.source) end |
#is_text? ⇒ Boolean
43 44 45 |
# File 'lib/immunio/plugins/action_view.rb', line 43 def is_text? @template.formats.first == :text end |
#load_source(context) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/immunio/plugins/action_view.rb', line 47 def load_source(context) return if !@template.respond_to?(:source) || !@template.source.nil? # @template is a virtual template that doesn't contain the source. We need # to try to load the source. But, the virtual template doesn't know the # original format of the source template file. # # First, try to load it using the Rails defaults (usually "html" and # "txt"). If that doesn't work, try to use the original format from the # virtual template. # # Though one might think the format from the virtual template would always # work, unfortunately the format from the template refers to the "type" of # the template, which may or may not be the same as the format of the # lookup context, which specifies the file extension of the template. Ugh, # naming... For example, the lookup context format may be "txt" while the # template format is "text". # # Astute readers may note that there's the possibility the template # extension is not in the Rails default list of lookup formats, and also # does not match the template type. We are just going to leave that for # another day, and hope that day never comes... begin refreshed = Immunio::IOHooks.paused { @template.refresh(context) } rescue begin old_formats = context.lookup_context.formats context.lookup_context.formats = @template.formats refreshed = Immunio::IOHooks.paused { @template.refresh(context) } rescue Immunio.logger.warn { "Failed to refresh template source from #{@template} using contexts #{old_formats} and #{@template.formats}" } ensure context.lookup_context.formats = old_formats end end return if refreshed.nil? @template.instance_variable_set :@source, refreshed.source end |
#mark_and_defer_fragment_write(key, content, options) ⇒ Object
171 172 173 174 175 176 |
# File 'lib/immunio/plugins/action_view.rb', line 171 def mark_and_defer_fragment_write(key, content, ) id = @scheduled_fragments_writes.size nonce = Template.get_nonce @scheduled_fragments_writes << [key, content, ] "{immunio-fragment:#{id}:#{nonce}}#{content}{/immunio-fragment:#{id}:#{nonce}}" end |
#next_template_id ⇒ Object
103 104 105 106 107 |
# File 'lib/immunio/plugins/action_view.rb', line 103 def next_template_id id = @next_template_id @next_template_id += 1 id end |
#next_var_id ⇒ Object
Generate the next var unique ID to be used in a template.
97 98 99 100 101 |
# File 'lib/immunio/plugins/action_view.rb', line 97 def next_var_id id = @next_var_id @next_var_id += 1 id end |
#render(context) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/immunio/plugins/action_view.rb', line 178 def render(context) load_source context # Don't handle templates with no source (inline text templates). unless has_source? rendered = yield rendered.instance_variable_set("@__immunio_processed", true) unless rendered.frozen? return rendered end begin root = true if rendering_stack.length == 0 rendering_stack.push self # Calculate SHA1 of this template. template_sha Immunio.logger.debug {"ActionView rendering template with sha #{@template_sha}, root: #{root}"} rendered = yield rendered.instance_variable_set("@__immunio_processed", true) unless rendered.frozen? if root # This is the root template. Let ActionView render it, and then look # for XSS. # If the rendered result isn't a string, or a string-like, then let's # skip it for safety sake. unless rendered.respond_to? :to_str unless $__immunio_av_rendered_non_string Immunio.logger.warn { "ActionView rendered #{@template.inspect} to a non-string-like value: #{rendered.inspect}. This rendering will not be analyzed for XSS. Further warnings will be suppressed." } $__immunio_av_rendered_non_string = true end return rendered end rendered = rendered.to_str result = run_hook!("template_render_done", { content_type: Mime::Type.lookup_by_extension(@template.formats.first).to_s, rendered: rendered, vars: @vars }) # We use the return value from the hook handler if present. rendered = result.fetch("rendered") { rendered.dup } remove_var_markers! rendered # If some fragments were marked to be cached, commit their content to cache. write_and_remove_fragments! context, rendered rendered.html_safe else # This is a partial template. Just render it. rendered end ensure top_template = rendering_stack.pop unless top_template == self raise Error, "Unexpected Immunio::Template on rendering stack. Expected #{id}, got #{top_template.try :id}." end end end |
#template_sha ⇒ Object
88 89 90 |
# File 'lib/immunio/plugins/action_view.rb', line 88 def template_sha CHECKSUM_CACHE[@template.object_id] end |