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
236 237 238 |
# File 'lib/immunio/plugins/action_view.rb', line 236 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 ‘<%= … %>`.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/immunio/plugins/action_view.rb', line 206 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
248 249 250 |
# File 'lib/immunio/plugins/action_view.rb', line 248 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
253 254 255 |
# File 'lib/immunio/plugins/action_view.rb', line 253 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
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 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 |
# File 'lib/immunio/plugins/action_view.rb', line 96 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.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
240 241 242 |
# File 'lib/immunio/plugins/action_view.rb', line 240 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
224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/immunio/plugins/action_view.rb', line 224 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
244 245 246 |
# File 'lib/immunio/plugins/action_view.rb', line 244 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
72 73 74 |
# File 'lib/immunio/plugins/action_view.rb', line 72 def compiled? @template.instance_variable_get :@compiled end |
#get_nonce ⇒ Object
89 90 91 92 93 94 |
# File 'lib/immunio/plugins/action_view.rb', line 89 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 |
# 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. Grab the original format # from the view context and override the default of just :html when # when looking up the template. old_formats = context.lookup_context.formats begin context.lookup_context.formats = @template.formats refreshed = @template.refresh(context) ensure context.lookup_context.formats = old_formats end return if refreshed.nil? @template.instance_variable_set :@source, refreshed.source end |
#mark_and_defer_fragment_write(key, content, options) ⇒ Object
151 152 153 154 155 156 |
# File 'lib/immunio/plugins/action_view.rb', line 151 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
83 84 85 86 87 |
# File 'lib/immunio/plugins/action_view.rb', line 83 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.
77 78 79 80 81 |
# File 'lib/immunio/plugins/action_view.rb', line 77 def next_var_id id = @next_var_id @next_var_id += 1 id end |
#render(context) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 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 |
# File 'lib/immunio/plugins/action_view.rb', line 158 def render(context) load_source context # Don't handle templates with no source (inline text templates). if not has_source? then rendered = yield rendered.instance_variable_set("@__immunio_processed", true) 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) if root # This is the root template. Let ActionView render it, and then look # for XSS. rendered = rendered.to_str # Rendering done! result = run_hook! "template_render_done", 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
68 69 70 |
# File 'lib/immunio/plugins/action_view.rb', line 68 def template_sha CHECKSUM_CACHE[@template.object_id] end |