700 lines
13 KiB
Ruby
700 lines
13 KiB
Ruby
require 'hanami/view'
|
|
require 'hanami/controller'
|
|
require 'hanami/helpers/html_helper'
|
|
require 'hanami/helpers/escape_helper'
|
|
require 'dry/struct'
|
|
|
|
module Types
|
|
include Dry::Types.module
|
|
end
|
|
|
|
Store = Struct.new(:code, :label) do
|
|
def to_ary
|
|
[label, code]
|
|
end
|
|
end
|
|
|
|
class Signup < Dry::Struct
|
|
attribute :password, Types::String.optional
|
|
end
|
|
|
|
class HtmlView
|
|
include Hanami::Helpers::HtmlHelper
|
|
|
|
def empty_div
|
|
html.div
|
|
end
|
|
|
|
def div_with_string_content
|
|
html.div('hello world')
|
|
end
|
|
|
|
def div_with_block_content_as_string
|
|
html.div { 'hola' }
|
|
end
|
|
|
|
def div_with_block_content_with_tag_helper
|
|
html.div(html.p('inner'))
|
|
end
|
|
|
|
def div_with_block_content_with_nested_calls
|
|
html.div do
|
|
span 'hello'
|
|
end
|
|
end
|
|
|
|
def div_with_block_content_and_multiple_nested_calls
|
|
html.form(action: '/users', method: 'POST') do
|
|
div do
|
|
label 'First name', for: 'user-first-name'
|
|
input type: 'text', id: 'user-first-name', name: 'user[first_name]', value: 'L'
|
|
end
|
|
|
|
input type: 'submit', value: 'Save changes'
|
|
end
|
|
end
|
|
|
|
def concatenation_of_multiple_fragments
|
|
hello = html { div 'Hello' }
|
|
hanami = html { div 'Hanami' }
|
|
|
|
hello + hanami
|
|
end
|
|
|
|
def concatenation_of_multiple_divs
|
|
html.div('Hello') + html.div('Hanami')
|
|
end
|
|
|
|
def concatenation_of_fragment_and_div
|
|
html { div 'Hello' } + html.div('Hanami')
|
|
end
|
|
|
|
def fragment_with_block_content
|
|
html do
|
|
div 'Hello'
|
|
div 'Hanami'
|
|
end
|
|
end
|
|
|
|
def div_with_attr
|
|
html.div(id: 'container')
|
|
end
|
|
|
|
# RUBY_VERSION >= '2.2'
|
|
# def div_with_data_attr
|
|
# html.div('data-where': 'up')
|
|
# end
|
|
def div_with_data_attr
|
|
html.div(:'data-where' => 'up')
|
|
end
|
|
|
|
def div_with_attrs
|
|
html.div(id: 'content', class: 'filled')
|
|
end
|
|
|
|
def div_with_string_content_and_attrs
|
|
html.div('ciao', id: 'greeting', class: 'blink')
|
|
end
|
|
|
|
def div_with_block_content_as_string_and_attrs
|
|
html.div(id: 'sidebar', class: 'blue') { 'bonjour' }
|
|
end
|
|
|
|
def custom_tag
|
|
html.tag(:custom, 'Foo', id: 'next')
|
|
end
|
|
|
|
def custom_empty_tag
|
|
html.empty_tag(:xr, id: 'next')
|
|
end
|
|
|
|
def evil_string_content
|
|
html.div("<script>alert('xss')</script>")
|
|
end
|
|
|
|
def evil_block_content
|
|
html.div { "<script>alert('xss')</script>" }
|
|
end
|
|
|
|
def evil_tag_helper
|
|
html.div(html.p("<script>alert('xss')</script>"))
|
|
end
|
|
|
|
def evil_nested_block_content
|
|
html.div do
|
|
p "<script>alert('xss')</script>"
|
|
end
|
|
end
|
|
end
|
|
|
|
class NumbersView
|
|
include Hanami::Helpers::NumberFormattingHelper
|
|
|
|
def single_digit
|
|
format_number 1
|
|
end
|
|
|
|
def thousand_digits
|
|
format_number 1_000
|
|
end
|
|
|
|
def million_digits
|
|
format_number 1_000_000
|
|
end
|
|
|
|
def zero_point_one
|
|
format_number 0.1
|
|
end
|
|
|
|
def precision_default_format
|
|
format_number 3.141592
|
|
end
|
|
|
|
def precision_format
|
|
format_number 3.141592, precision: 4
|
|
end
|
|
|
|
def precision_higher_than_numbers_precision
|
|
format_number 3.14, precision: 4
|
|
end
|
|
|
|
def zero_precision
|
|
format_number 3.14, precision: 0
|
|
end
|
|
|
|
def mixed_digits
|
|
format_number 5002.007, precision: 3
|
|
end
|
|
|
|
def euro_format
|
|
format_number 1234.12, delimiter: '.', separator: ','
|
|
end
|
|
|
|
def pass_nil
|
|
format_number nil
|
|
end
|
|
|
|
def pass_class_name
|
|
format_number Object
|
|
end
|
|
|
|
def pass_string
|
|
format_number 'string'
|
|
end
|
|
|
|
def pass_non_numeric_integer
|
|
format_number '1'
|
|
end
|
|
|
|
def pass_non_numeric_float
|
|
format_number '1.0'
|
|
end
|
|
|
|
def big_decimal
|
|
format_number BigDecimal.new('0.0001'), precision: 4
|
|
end
|
|
|
|
def complex
|
|
format_number Complex(1)
|
|
end
|
|
|
|
def rational
|
|
format_number Rational(1)
|
|
end
|
|
|
|
def string
|
|
format_number Rational(1)
|
|
end
|
|
|
|
def infinity
|
|
format_number Float::INFINITY
|
|
end
|
|
|
|
def nan
|
|
format_number 0.0 / 0
|
|
end
|
|
end
|
|
|
|
class EscapeView
|
|
include Hanami::Helpers::EscapeHelper
|
|
|
|
def good_string
|
|
escape_html 'this is a good string'
|
|
end
|
|
|
|
def evil_string
|
|
escape_html "<script>alert('xss')</script>"
|
|
end
|
|
|
|
def good_attributes_string
|
|
"<a title='#{escape_html_attribute('foo')}'>link</a>"
|
|
end
|
|
|
|
def evil_attributes_string
|
|
"<a title='#{escape_html_attribute('<script>alert(\'xss\')</script>')}'>link</a>"
|
|
end
|
|
|
|
def good_url_string
|
|
escape_url 'http://hanamirb.org'
|
|
end
|
|
|
|
def evil_url_string
|
|
escape_url "javascript:alert('xss')"
|
|
end
|
|
|
|
def raw_string
|
|
raw "<div>I'm a raw string</div>"
|
|
end
|
|
|
|
def html_string_alias
|
|
h 'this is a good string'
|
|
end
|
|
|
|
def html_attribute_string_alias
|
|
"<a title='#{ha('foo')}'>link</a>"
|
|
end
|
|
|
|
def url_string_alias
|
|
hu 'http://hanamirb.org'
|
|
end
|
|
end
|
|
|
|
class Book < Dry::Struct
|
|
constructor_type :weak
|
|
|
|
attribute :title, Types::String.optional
|
|
attribute :search_title, Types::String.optional
|
|
attribute :description, Types::String.optional
|
|
attribute :author_id, Types::Form::Int.optional
|
|
attribute :category, Types::String.optional
|
|
attribute :cover, Types::String.optional
|
|
attribute :image_cover, Types::String.optional
|
|
attribute :percent_read, Types::Form::Int.optional
|
|
attribute :discount_percentage, Types::Form::Int.optional
|
|
attribute :published_at, Types::String.optional
|
|
attribute :website, Types::String.optional
|
|
attribute :publisher_email, Types::String.optional
|
|
attribute :publisher_telephone, Types::String.optional
|
|
attribute :release_date, Types::Form::Date.optional
|
|
attribute :release_hour, Types::String.optional
|
|
attribute :release_week, Types::String.optional
|
|
attribute :release_month, Types::Form::Date.optional
|
|
attribute :store, Types::String.optional
|
|
end
|
|
|
|
User = Struct.new(:name, :website, :snippet)
|
|
|
|
module TestView
|
|
def self.included(view)
|
|
view.class_eval do
|
|
include Hanami::View
|
|
include Hanami::Helpers
|
|
root __dir__ + '/fixtures/templates'
|
|
end
|
|
end
|
|
end
|
|
|
|
module Books
|
|
class Show
|
|
include TestView
|
|
|
|
def title_widget
|
|
html.div do
|
|
h1 book.title
|
|
end
|
|
end
|
|
end
|
|
|
|
class Error
|
|
include TestView
|
|
|
|
def error_widget
|
|
html.div do
|
|
unknown_local_variable
|
|
end
|
|
end
|
|
|
|
def render
|
|
error_widget.to_s
|
|
end
|
|
end
|
|
end
|
|
|
|
module Users
|
|
class Show
|
|
include TestView
|
|
|
|
def title
|
|
html.h1(user.name)
|
|
end
|
|
|
|
def details
|
|
html.div(id: 'details') do
|
|
ul do
|
|
li do
|
|
a('website', href: hu(user.website), title: "#{ha(user.name)}'s website")
|
|
end
|
|
|
|
li raw(user.snippet)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class FormHelperView
|
|
include Hanami::Helpers::FormHelper
|
|
attr_reader :params
|
|
|
|
def initialize(params)
|
|
@params = _build_params(params)
|
|
end
|
|
|
|
private
|
|
|
|
def _build_params(params)
|
|
parameters = params.to_h
|
|
|
|
# Randomly use Hanami::Action::BaseParams or the given raw Hash in order to
|
|
# simulate Hash usage during the test setup unit tests in Hanami projects.
|
|
if parameters.respond_to?(:dig)
|
|
[true, false].sample ? Hanami::Action::BaseParams.new(parameters) : parameters
|
|
else
|
|
Hanami::Action::BaseParams.new(parameters)
|
|
end
|
|
end
|
|
end
|
|
|
|
class SessionFormHelperView < FormHelperView
|
|
def initialize(params, csrf_token)
|
|
super(params)
|
|
@csrf_token = csrf_token
|
|
end
|
|
|
|
def session
|
|
{ _csrf_token: @csrf_token }
|
|
end
|
|
end
|
|
|
|
class Address
|
|
attr_reader :street
|
|
|
|
def initialize(attributes = {})
|
|
@street = attributes[:street]
|
|
end
|
|
end
|
|
|
|
class Delivery
|
|
attr_reader :id, :customer_id, :address
|
|
|
|
def initialize(attributes = {})
|
|
@id = attributes[:id]
|
|
@customer_id = attributes[:customer_id]
|
|
@address = attributes[:address]
|
|
end
|
|
end
|
|
|
|
class Bill
|
|
attr_reader :id, :addresses
|
|
|
|
def initialize(attributes = {})
|
|
@id = attributes[:id]
|
|
@addresses = attributes[:addresses]
|
|
end
|
|
end
|
|
|
|
class DeliveryParams < Hanami::Action::Params
|
|
params do
|
|
required(:delivery).schema do
|
|
required(:customer_id, :int).filled
|
|
required(:address).schema do
|
|
required(:street, :string).filled
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class BillParams < Hanami::Action::Params
|
|
params do
|
|
required(:bill).schema do
|
|
required(:addresses).each do
|
|
schema do
|
|
required(:street, :string).filled
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class Session
|
|
def initialize(values)
|
|
@values = values.to_h
|
|
end
|
|
|
|
def [](key)
|
|
@values[key]
|
|
end
|
|
end
|
|
|
|
module FullStack
|
|
class Routes
|
|
def path(name)
|
|
_escape "/#{name}"
|
|
end
|
|
|
|
def sessions_path
|
|
_escape '/sessions'
|
|
end
|
|
|
|
def deliveries_path
|
|
_escape '/deliveries'
|
|
end
|
|
|
|
def delivery_path(attrs = {})
|
|
_escape "/deliveries/#{attrs.fetch(:id)}"
|
|
end
|
|
|
|
def bill_path(attrs = {})
|
|
_escape "/bills/#{attrs.fetch(:id)}"
|
|
end
|
|
|
|
private
|
|
|
|
def _escape(string)
|
|
Hanami::Utils::Escape::SafeString.new(string)
|
|
end
|
|
end
|
|
|
|
def self.routes
|
|
Routes.new
|
|
end
|
|
|
|
module Views
|
|
module Dashboard
|
|
class Index
|
|
include TestView
|
|
root __dir__ + '/fixtures/templates/full_stack'
|
|
template 'dashboard/index'
|
|
|
|
def routing_helper_path
|
|
routes.path(:dashboard)
|
|
end
|
|
end
|
|
end
|
|
|
|
module Sessions
|
|
class New
|
|
include TestView
|
|
template 'sessions/new'
|
|
end
|
|
end
|
|
|
|
module Cart
|
|
class Show
|
|
include TestView
|
|
template 'cart/show'
|
|
|
|
def total
|
|
format_number locals[:total]
|
|
end
|
|
end
|
|
end
|
|
|
|
module Deliveries
|
|
class New
|
|
include TestView
|
|
template 'deliveries/new'
|
|
|
|
def form
|
|
Form.new(:delivery, routes.deliveries_path)
|
|
end
|
|
|
|
def submit_label
|
|
'Create'
|
|
end
|
|
end
|
|
|
|
class Edit
|
|
include TestView
|
|
template 'deliveries/edit'
|
|
|
|
def form
|
|
Form.new(:delivery,
|
|
routes.delivery_path(id: delivery.id),
|
|
{ delivery: delivery }, method: :patch)
|
|
end
|
|
|
|
def submit_label
|
|
'Update'
|
|
end
|
|
end
|
|
end
|
|
|
|
module Bills
|
|
class Edit
|
|
include TestView
|
|
template 'bills/edit'
|
|
|
|
def form
|
|
Form.new(:bill, routes.bill_path(id: bill.id), { bill: bill }, method: :patch)
|
|
end
|
|
|
|
def submit_label
|
|
'Update'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class ViewWithoutRoutingHelper
|
|
include TestView
|
|
root __dir__ + '/fixtures/templates/full_stack'
|
|
template 'dashboard/index'
|
|
|
|
def routing_helper_path
|
|
routes.path(:dashboard)
|
|
end
|
|
end
|
|
|
|
class LinkTo
|
|
include Hanami::Helpers::LinkToHelper
|
|
|
|
class Index
|
|
include TestView
|
|
|
|
def link_to_home
|
|
link_to('Home', '/')
|
|
end
|
|
|
|
def link_to_relative
|
|
link_to('Relative', 'relative')
|
|
end
|
|
|
|
def link_to_home_with_html_content
|
|
link_to('/') do
|
|
p 'Home with html content'
|
|
end
|
|
end
|
|
|
|
def link_to_home_with_html_content_id_and_class
|
|
link_to('/', id: 'home__link', class: 'first') do
|
|
p 'Home with html content, id and class'
|
|
end
|
|
end
|
|
|
|
def link_to_external_url_with_content
|
|
link_to('External', 'http://external.com')
|
|
end
|
|
|
|
def link_to_external_url_with_html_content
|
|
link_to('http://external.com') do
|
|
strong 'External with html content'
|
|
end
|
|
end
|
|
|
|
def link_to_xss_content
|
|
link_to(%(<script>alert('xss')</script>), '/')
|
|
end
|
|
|
|
def link_to_xss_raw_content_block
|
|
link_to('/') do
|
|
%(<script>alert('xss2')</script>)
|
|
end
|
|
end
|
|
|
|
def link_to_xss_html_builder_content_block
|
|
link_to('/') do
|
|
p %(<script>alert('xss3')</script>)
|
|
end
|
|
end
|
|
end
|
|
|
|
class Routes
|
|
def self.path(name, id = nil)
|
|
"/#{name}" << "/#{id}"
|
|
end
|
|
end
|
|
|
|
def routes
|
|
Routes
|
|
end
|
|
|
|
def link_to_relative_posts
|
|
link_to('Posts', 'posts')
|
|
end
|
|
|
|
def link_to_posts
|
|
link_to('Posts', routes.path(:posts))
|
|
end
|
|
|
|
def link_to_post
|
|
link_to('Post', routes.path(:post, 1))
|
|
end
|
|
|
|
def link_to_with_class
|
|
link_to('Post', routes.path(:posts), class: 'first')
|
|
end
|
|
|
|
def link_to_with_id
|
|
link_to('Post', routes.path(:posts), id: 'posts__link')
|
|
end
|
|
|
|
def link_to_with_html_content
|
|
link_to(routes.path(:posts)) do
|
|
strong 'Post'
|
|
end
|
|
end
|
|
|
|
def link_to_with_content_and_html_content
|
|
link_to('Post', routes.path(:posts)) do
|
|
strong 'Post'
|
|
end
|
|
end
|
|
|
|
def link_to_with_html_content_id_and_class
|
|
link_to(routes.path(:posts), id: 'posts__link', class: 'first') do
|
|
strong 'Post'
|
|
end
|
|
end
|
|
|
|
def link_to_without_args
|
|
link_to
|
|
end
|
|
|
|
def link_to_without_args_and_empty_block
|
|
link_to do
|
|
# this block was left intentionally blank ;)
|
|
end
|
|
end
|
|
|
|
def link_to_with_only_content
|
|
link_to 'Post'
|
|
end
|
|
|
|
def link_to_with_content_html_content_id_and_class
|
|
link_to('Post', routes.path(:posts), id: 'posts__link', class: 'first') do
|
|
strong 'Post'
|
|
end
|
|
end
|
|
end
|
|
|
|
class HtmlAndLinkTo
|
|
include Hanami::Helpers::HtmlHelper
|
|
include Hanami::Helpers::LinkToHelper
|
|
|
|
def two_links_to_in_div
|
|
html.div do
|
|
link_to('Comments', '/comments') +
|
|
link_to('Posts', '/posts')
|
|
end
|
|
end
|
|
|
|
def span_and_link_to_in_div
|
|
html.div do
|
|
span('hello') +
|
|
link_to('Comments', '/comments')
|
|
end
|
|
end
|
|
end
|