# frozen_string_literal: true require "temple" require "uri" module Hanami class View module Helpers # Helper methods for escaping content for safely including in HTML. # # When using full Hanami apps, these helpers will be automatically available in your view # templates, part classes and scope classes. # # When using hanami-view standalone, include this module directly in your base part and scope # classes, or in specific classes as required. # # @example Standalone usage # class BasePart < Hanami::View::Part # include Hanami::View::Helpers::EscapeHelper # end # # class BaseScope < Hanami::View::Scope # include Hanami::View::Helpers::EscapeHelper # end # # class BaseView < Hanami::View # config.part_class = BasePart # config.scope_class = BaseScope # end # # @api public # @since 2.0.0 module EscapeHelper module_function # Returns an escaped string that is safe to include in HTML. # # Use this helper when including any untrusted user input in your view content. # # If the given string is already marked as HTML safe, then it will be returned without # escaping. # # Marks the escaped string marked as HTML safe, ensuring it will not be escaped again. # # @param input [String] the input string # @return [Hanami::View::HTML::SafeString] the escaped string # # @example # escape_html("Safe content") # # => "Safe content" # # escape_html("") # # => "<script>alert('xss')</script>" # # escape_html(raw("

Not escaped

")) # # => "

Not escaped

" # # @api public # @since 2.0.0 def escape_html(input) Temple::Utils.escape_html_safe(input) end # @api public # @since 2.0.0 alias_method :h, :escape_html # Returns an escaped string from joining the elements in a given array. # # Behaves similarly to `Array#join`. The given array is flattened, and all items, including # the supplied separator, are HTML escaped unless they are already HTML safe. # # Marks the returned string as HTML safe, ensuring it will not be escaped again. # # @param array [Array<#to_s>] the array # @param separator[String] the separator for the joined string # @return [Hanami::View::HTML::SafeString] the escaped string # # @example # safe_join([raw("

foo

"), "

bar

"], "
") # # => "

foo

<br><p>bar</p>" # # safe_join([raw("

foo

"), raw("

bar

")], raw("
")) # # => "

foo


bar

" # # @see #escape_html # # @api public # @since 2.0.0 def escape_join(array, separator = $,) separator = escape_html(separator) array.flatten.map! { |i| escape_html(i) }.join(separator).html_safe end # Returns a the given URL string if it has one of the permitted URL schemes. For URLs with # non-permitted schemes, returns an empty string. # # Use this method when including URLs from untrusted user input in your view content. # # The default permitted schemes are: # - `http` # - `https` # - `mailto` # # @param input [String] the URL string # @param permitted_schemes [Array] an optional array of permitted schemes # # @return [String] the permitted URL, or empty string # # @example # sanitize_url("https://hanamirb.org") # => "http://hanamirb.org" # sanitize_url("javascript:alert('xss')") # => "" # # sanitize_url("gemini://gemini.circumlunar.space/", %w[http https gemini]) # # => "gemini://gemini.circumlunar.space/" # # @api public # @since 2.0.0 def sanitize_url(input, permitted_schemes = PERMITTED_URL_SCHEMES) return input if input.html_safe? URI::DEFAULT_PARSER.extract( URI.decode_www_form_component(input.to_s), permitted_schemes ).first.to_s.html_safe end # @api private # @since 2.0.0 PERMITTED_URL_SCHEMES = %w[http https mailto].freeze private_constant :PERMITTED_URL_SCHEMES # Returns an escaped name from the given string, intended for use as an XML tag or attribute # name. # # Replaces non-safe characters with an underscore. # # Follows the requirements of the [XML specification](https://www.w3.org/TR/REC-xml/#NT-Name). # # @example # escape_xml_name("1 < 2 & 3") # => "1___2___3" # # @api public # @since 2.0.0 def escape_xml_name(name) name = name.to_s return "" if name.match?(BLANK_STRING_REGEXP) return name if name.match?(SAFE_XML_TAG_NAME_REGEXP) starting_char = name[0] starting_char.gsub!(INVALID_TAG_NAME_START_REGEXP, TAG_NAME_REPLACEMENT_CHAR) return starting_char if name.size == 1 following_chars = name[1..-1] following_chars.gsub!(INVALID_TAG_NAME_FOLLOWING_REGEXP, TAG_NAME_REPLACEMENT_CHAR) starting_char << following_chars end # @api private # @since 2.0.0 BLANK_STRING_REGEXP = /\A\s*\z/ # Following XML requirements: https://www.w3.org/TR/REC-xml/#NT-Name # @api private # @since 2.0.0 TAG_NAME_START_CODEPOINTS = \ "@:A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}" \ "\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}" \ "\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}" private_constant :TAG_NAME_START_CODEPOINTS # @api private # @since 2.0.0 INVALID_TAG_NAME_START_REGEXP = /[^#{TAG_NAME_START_CODEPOINTS}]/ private_constant :INVALID_TAG_NAME_START_REGEXP # @api private # @since 2.0.0 TAG_NAME_FOLLOWING_CODEPOINTS = "#{TAG_NAME_START_CODEPOINTS}\\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}" private_constant :TAG_NAME_FOLLOWING_CODEPOINTS # @api private # @since 2.0.0 INVALID_TAG_NAME_FOLLOWING_REGEXP = /[^#{TAG_NAME_FOLLOWING_CODEPOINTS}]/ private_constant :INVALID_TAG_NAME_FOLLOWING_REGEXP # @api private # @since 2.0.0 SAFE_XML_TAG_NAME_REGEXP = /\A[#{TAG_NAME_START_CODEPOINTS}][#{TAG_NAME_FOLLOWING_CODEPOINTS}]*\z/ private_constant :INVALID_TAG_NAME_FOLLOWING_REGEXP # @api private # @since 2.0.0 TAG_NAME_REPLACEMENT_CHAR = "_" private_constant :TAG_NAME_REPLACEMENT_CHAR # Returns the given string marked as HTML safe, meaning it will not be escaped when included # in your view's HTML. # # This is NOT recommended if the string is coming from untrusted user input. Use at your own # peril. # # @param input [String] the input # @return [Hanami::View::HTML::SafeString] the string marked as HTML safe # # @example # raw(user.name) # => "Little Bobby Tables" # raw(user.name).html_safe? # => true # # @api public # @since 2.0.0 def raw(input) input.to_s.html_safe end end end end end