require 'hanami/utils/escape'
module Hanami
module View
# Auto escape logic for views and presenters.
#
# @since 0.4.0
module Escape
module InstanceMethods
private
# Mark the given string as safe to render.
#
# !!! ATTENTION !!! This may open your application to XSS attacks.
#
# @param string [String] the input string
#
# @return [Hanami::Utils::Escape::SafeString] the string marked as safe
#
# @since 0.4.0
# @api public
#
# @example View usage
# require 'hanami/view'
#
# User = Struct.new(:name)
#
# module Users
# class Show
# include Hanami::View
#
# def user_name
# _raw user.name
# end
# end
# end
#
# # ERB template
# #
<%= user_name %>
#
# user = User.new("")
# html = Users::Show.render(format: :html, user: user)
#
# html # =>
#
# @example Presenter usage
# require 'hanami/view'
#
# User = Struct.new(:name)
#
# class UserPresenter
# include Hanami::Presenter
#
# def name
# _raw @object.name
# end
# end
#
# user = User.new("")
# presenter = UserPresenter.new(user)
#
# presenter.name # => ""
def _raw(string)
::Hanami::Utils::Escape::SafeString.new(string)
end
# Force the output escape for the given object
#
# @param object [Object] the input object
#
# @return [Hanami::View::Escape::Presenter] a presenter with output
# autoescape
#
# @since 0.4.0
# @api public
#
# @see Hanami::View::Escape::Presenter
#
# @example View usage
# require 'hanami/view'
#
# User = Struct.new(:first_name, :last_name)
#
# module Users
# class Show
# include Hanami::View
#
# def user
# _escape locals[:user]
# end
# end
# end
#
# # ERB template:
# #
# #
# # <%= user.first_name %>
# #
# #
# # <%= user.last_name %>
# #
#
# first_name = ""
# last_name = ""
#
# user = User.new(first_name, last_name)
# html = Users::Show.render(format: :html, user: user)
#
# html
# # =>
# #
# # <script>alert('first_name')</script>
# #
# #
# # <script>alert('last_name')</script>
# #
def _escape(object)
::Hanami::View::Escape::Presenter.new(object)
end
end
module Presentable
# Inject escape logic into the given class.
#
# @since 0.7.0
# @api private
#
# @see Hanami::View::Escape
def self.included(base)
base.extend ::Hanami::View::Escape
end
# Initialize the presenter
#
# @param object [Object] the object to present
#
# @since 0.7.0
def initialize(object)
@object = object
end
protected
# Override Ruby's method_missing
#
# @api private
# @since 0.7.0
def method_missing(m, *args, &blk)
if @object.respond_to?(m)
::Hanami::View::Escape.html(@object.__send__(m, *args, &blk))
else
super
end
end
# Override Ruby's respond_to_missing? in order to support proper delegation
#
# @api private
# @since 0.3.0
def respond_to_missing?(m, include_private = false)
@object.respond_to?(m, include_private)
end
end
# Auto escape presenter
#
# @since 0.4.0
# @api private
#
# @see Hanami::View::Escape::InstanceMethods#_escape
class Presenter
include Presentable
end
# Escape the given input if it's a string, otherwise return the oject as it is.
#
# @param input [Object] the input
#
# @return [Object,String] the escaped string or the given object
#
# @since 0.4.0
# @api private
def self.html(input)
case input
when String
Utils::Escape.html(input)
else
input
end
end
# Module extended override
#
# @since 0.4.0
# @api private
def self.extended(base)
base.class_eval do
include ::Hanami::Utils::ClassAttribute
include ::Hanami::View::Escape::InstanceMethods
class_attribute :autoescape_methods
self.autoescape_methods = {}
end
end
# Wraps concrete view methods with escape logic.
#
# @since 0.4.0
# @api private
def method_added(method_name)
visibility = :public
visibility = :private if private_method_defined? method_name
visibility = :protected if protected_method_defined? method_name
unless autoescape_methods[method_name]
prepend Module.new {
module_eval %{
#{ visibility } def #{ method_name }(*args, &blk); ::Hanami::View::Escape.html super; end
}
}
autoescape_methods[method_name] = true
end
end
end
end
end