brut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- metadata +484 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
# An InputDefinition captures metadata used to create an Input. Think of this
|
2
|
+
# as a template for creating inputs. An Input has state, such as values and thus validity.
|
3
|
+
# An InputDefinition is immutable and defines inputs.
|
4
|
+
class Brut::FrontEnd::Forms::InputDefinition
|
5
|
+
include Brut::Framework::FussyTypeEnforcement
|
6
|
+
attr_reader :max,
|
7
|
+
:maxlength,
|
8
|
+
:min,
|
9
|
+
:minlength,
|
10
|
+
:name,
|
11
|
+
:pattern,
|
12
|
+
:required,
|
13
|
+
:step,
|
14
|
+
:type
|
15
|
+
|
16
|
+
INPUT_TYPES_TO_CLASS = {
|
17
|
+
"checkbox" => String,
|
18
|
+
"color" => String,
|
19
|
+
"date" => String,
|
20
|
+
"datetime-local" => String,
|
21
|
+
"email" => String,
|
22
|
+
"file" => String,
|
23
|
+
"hidden" => String,
|
24
|
+
"month" => String,
|
25
|
+
"number" => Numeric,
|
26
|
+
"password" => String,
|
27
|
+
"radio" => String,
|
28
|
+
"range" => String,
|
29
|
+
"search" => String,
|
30
|
+
"tel" => String,
|
31
|
+
"text" => String,
|
32
|
+
"time" => String,
|
33
|
+
"url" => String,
|
34
|
+
"week" => String,
|
35
|
+
}
|
36
|
+
|
37
|
+
# Create an InputDefinition. This should very closely mirror
|
38
|
+
# the attributes used in an <INPUT> element in HTML.
|
39
|
+
def initialize(
|
40
|
+
max: nil,
|
41
|
+
maxlength: nil,
|
42
|
+
min: nil,
|
43
|
+
minlength: nil,
|
44
|
+
name: nil,
|
45
|
+
pattern: nil,
|
46
|
+
required: :based_on_type,
|
47
|
+
step: nil,
|
48
|
+
type: nil
|
49
|
+
)
|
50
|
+
name = name.to_s
|
51
|
+
type = if type.nil?
|
52
|
+
case name
|
53
|
+
when "email" then "email"
|
54
|
+
when "password" then "password"
|
55
|
+
else
|
56
|
+
"text"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
type
|
60
|
+
end
|
61
|
+
|
62
|
+
type = type.to_s
|
63
|
+
if required == :based_on_type
|
64
|
+
required = type != "checkbox"
|
65
|
+
end
|
66
|
+
|
67
|
+
@max = type!( max , Numeric , "max")
|
68
|
+
@maxlength = type!( maxlength , Numeric , "maxlength")
|
69
|
+
@min = type!( min , Numeric , "min")
|
70
|
+
@minlength = type!( minlength , Numeric , "minlength")
|
71
|
+
@name = type!( name , String , "name")
|
72
|
+
@pattern = type!( pattern , String , "pattern")
|
73
|
+
@required = type!( required , [true, false] , "required", required: true)
|
74
|
+
@step = type!( step , Numeric , "step")
|
75
|
+
@type = type!( type , INPUT_TYPES_TO_CLASS.keys , "type", required: true)
|
76
|
+
|
77
|
+
if @pattern.nil? && type == "email"
|
78
|
+
@pattern = /^[^@]+@[^@]+\.[^@]+$/.source
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create an Input based on this defitition, initializing it with the given value.
|
83
|
+
def make_input(value:)
|
84
|
+
Brut::FrontEnd::Forms::Input.new(input_definition: self, value: value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
class Brut::FrontEnd::Forms::SelectInputDefinition
|
88
|
+
include Brut::Framework::FussyTypeEnforcement
|
89
|
+
attr_reader :required, :name
|
90
|
+
def initialize(name:, required: true)
|
91
|
+
name = name.to_s
|
92
|
+
@name = type!( name , String , "name")
|
93
|
+
@required = type!( required , [true, false] , "required", required:true)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Create an Input based on this defitition, initializing it with the given value.
|
97
|
+
def make_input(value:)
|
98
|
+
Brut::FrontEnd::Forms::SelectInput.new(input_definition: self, value: value)
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Mirrors a web browser's ValidityState API. Captures the overall state
|
2
|
+
# of validity of an input. This can accomodate server-side constraint violations
|
3
|
+
# that are essentially arbitrary. This means that an instance of this class should
|
4
|
+
# fully capture all constraint violations for a given field. You can
|
5
|
+
# iterate over all the violations with #each, which will yield one `ConstraintViolation` for
|
6
|
+
# each failure. You can query the constraint to determine if it is a client side constraint or not.
|
7
|
+
class Brut::FrontEnd::Forms::ValidityState
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(constraint_violations={})
|
11
|
+
@constraint_violations = constraint_violations.map { |key,value|
|
12
|
+
if value
|
13
|
+
Brut::FrontEnd::Forms::ConstraintViolation.new(key: key, context: {})
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if there are no validation errors
|
21
|
+
def valid? = @constraint_violations.empty?
|
22
|
+
|
23
|
+
# Set a server-side constraint violation. This is essentially arbitrary and dependent
|
24
|
+
# on your use-case.
|
25
|
+
def server_side_constraint_violation(key:,context:)
|
26
|
+
@constraint_violations << Brut::FrontEnd::Forms::ConstraintViolation.new(key: key, context: context, server_side: true)
|
27
|
+
end
|
28
|
+
|
29
|
+
def each(&block)
|
30
|
+
@constraint_violations.each do |constraint|
|
31
|
+
block.call(constraint)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# A handler responds to all HTTP requests other than those that render a page. It will be given any data it needs
|
2
|
+
# to handle the request to its handle method. You define this method to accept the parameters you expect.
|
3
|
+
#
|
4
|
+
# You may also define before_handle which will be given any subset of those parameters and can perform logic before
|
5
|
+
# handle is called. This is most useful in a base class to check for permissions or other cross-cutting concerns.
|
6
|
+
#
|
7
|
+
# Tests should call handle!
|
8
|
+
module Brut::FrontEnd
|
9
|
+
class Handler
|
10
|
+
include Brut::FrontEnd::HandlingResults
|
11
|
+
|
12
|
+
def handle(**)
|
13
|
+
raise Brut::Framework::Errors::AbstractMethod
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle!(**args)
|
17
|
+
result = nil
|
18
|
+
if self.respond_to?(:before_handle)
|
19
|
+
before_handle_args = self.method(:before_handle).parameters.map { |(type,name)|
|
20
|
+
if type == :keyreq
|
21
|
+
if args.key?(name)
|
22
|
+
[ name, args[name] ]
|
23
|
+
else
|
24
|
+
raise ArgumentError,"before_handle requires keyword arg '#{name}' but `handle` did not receive it. It must"
|
25
|
+
end
|
26
|
+
elsif type == :key
|
27
|
+
if args.key?(name)
|
28
|
+
[ name, args[name] ]
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
else
|
33
|
+
raise ArgumentError,"before_handle must only have keyword args. Got '#{name}' of type '#{type}'"
|
34
|
+
end
|
35
|
+
}.compact.to_h
|
36
|
+
result = self.before_handle(**before_handle_args)
|
37
|
+
end
|
38
|
+
if result.nil?
|
39
|
+
result = self.handle(**args)
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
module Handlers
|
45
|
+
autoload(:CspReportingHandler,"brut/front_end/handlers/csp_reporting_handler")
|
46
|
+
autoload(:LocaleDetectionHandler,"brut/front_end/handlers/locale_detection_handler")
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Brut::FrontEnd::Handlers::CspReportingHandler < Brut::FrontEnd::Handler
|
2
|
+
def handle(body:)
|
3
|
+
begin
|
4
|
+
parsed = JSON.parse(body.read)
|
5
|
+
SemanticLogger["brut:__brut/csp-reporting"].info(parsed)
|
6
|
+
rescue => ex
|
7
|
+
SemanticLogger["brut:__brut/locale"].warn("Got #{ex} from /__brut/locale instead of a parseable JSON object")
|
8
|
+
end
|
9
|
+
http_status(200)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Brut::FrontEnd::Handlers::LocaleDetectionHandler < Brut::FrontEnd::Handler
|
2
|
+
def handle(body:,session:)
|
3
|
+
begin
|
4
|
+
parsed = JSON.parse(body.read)
|
5
|
+
SemanticLogger["brut:__brut/locale"].info("Got #{parsed.class}/#{parsed}")
|
6
|
+
if parsed.kind_of?(Hash)
|
7
|
+
locale = parsed["locale"]
|
8
|
+
timezone = parsed["timeZone"]
|
9
|
+
|
10
|
+
session.timezone_from_browser = timezone
|
11
|
+
if !session.http_accept_language.known?
|
12
|
+
session.http_accept_language = Brut::I18n::HTTPAcceptLanguage.from_browser(locale)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
SemanticLogger["brut:__brut/locale"].warn("Got a #{parsed.class} from /__brut/locale instead of a hash")
|
16
|
+
end
|
17
|
+
rescue => ex
|
18
|
+
SemanticLogger["brut:__brut/locale"].warn("Got #{ex} from /__brut/locale instead of a parseable JSON object")
|
19
|
+
end
|
20
|
+
http_status(200)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Brut::FrontEnd::HandlingResults
|
2
|
+
# For use inside handle! or process! to indicate the user should be redirected to
|
3
|
+
# the route for the given class and query string parameters. If the route
|
4
|
+
# does not support GET, an exception is raised
|
5
|
+
def redirect_to(klass, **query_string_params)
|
6
|
+
if !klass.kind_of?(Class)
|
7
|
+
raise ArgumentError,"redirect_to should be given a Class, not a #{klass.class}"
|
8
|
+
end
|
9
|
+
Brut.container.routing.uri(klass,with_method: :get,**query_string_params)
|
10
|
+
end
|
11
|
+
|
12
|
+
# For use when an HTTP status code must be returned.
|
13
|
+
def http_status(number) = Brut::FrontEnd::HttpStatus.new(number)
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Brut::FrontEnd::HttpMethod
|
2
|
+
def initialize(string)
|
3
|
+
normalized = string.to_s.downcase.to_sym
|
4
|
+
if !self.class.method_names.include?(normalized)
|
5
|
+
raise ArgumentError,"'#{string}' is not a known HTTP method"
|
6
|
+
end
|
7
|
+
@method = normalized
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s = @method.to_s
|
11
|
+
def to_sym = @method.to_sym
|
12
|
+
alias to_str to_s
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
self.class.name == other.class.name && self.to_s == other.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def get? = self.to_sym == :get
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.method_names = [
|
23
|
+
:connect,
|
24
|
+
:delete,
|
25
|
+
:get,
|
26
|
+
:head,
|
27
|
+
:options,
|
28
|
+
:patch,
|
29
|
+
:post,
|
30
|
+
:put,
|
31
|
+
:trace,
|
32
|
+
].freeze
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Brut::FrontEnd::HttpStatus
|
2
|
+
def initialize(number)
|
3
|
+
number = number.to_i
|
4
|
+
if ((number < 100) || (number > 599))
|
5
|
+
raise ArgumentError,"'#{number}' is not a known HTTP status code"
|
6
|
+
end
|
7
|
+
@number = number
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_i = @number
|
11
|
+
def to_s = to_i.to_s
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
self.class == other.class && self.to_i == other.to_i
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Brut::FrontEnd::Middlewares::ReloadApp < Brut::FrontEnd::Middleware
|
2
|
+
LOCK = Concurrent::ReadWriteLock.new
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
def call(env)
|
7
|
+
Brut.container.instrumentation.instrument(Brut::Instrumentation::Event.new(category: "middleware", subcategory: self.class.name, name: "call")) do
|
8
|
+
# We can only have one thread reloading stuff at a time, per process.
|
9
|
+
# The ReadWriteLock achieves this.
|
10
|
+
#
|
11
|
+
# Here, if any thread is serving a request, THIS thread will wait here.
|
12
|
+
# Once no other thread is serving a request, the write lock is acquired and a reload happens.
|
13
|
+
LOCK.with_write_lock do
|
14
|
+
begin
|
15
|
+
Brut.container.zeitwerk_loader.reload
|
16
|
+
Brut.container.routing.reload
|
17
|
+
Brut.container.asset_path_resolver.reload
|
18
|
+
::I18n.reload!
|
19
|
+
rescue => ex
|
20
|
+
SemanticLogger[self.class].warn("Reload failed - your browser may not show you the latest code: #{ex.message}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# If another thread has a write lock, we wait here so that the reload can complete before serving
|
24
|
+
# the request. If no thread has a write lock, THIS thread may proceed to serve the request,
|
25
|
+
# as will any other thread that gets here.
|
26
|
+
LOCK.with_read_lock do
|
27
|
+
@app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# A page is a component that has a layout and thus is intended to be
|
2
|
+
# an entire web page, not just a fragment.
|
3
|
+
class Brut::FrontEnd::Page < Brut::FrontEnd::Component
|
4
|
+
include Brut::FrontEnd::HandlingResults
|
5
|
+
using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
|
6
|
+
|
7
|
+
def layout = "default"
|
8
|
+
|
9
|
+
def before_render = nil
|
10
|
+
|
11
|
+
def handle!
|
12
|
+
case before_render
|
13
|
+
in URI => uri
|
14
|
+
uri
|
15
|
+
in Brut::FrontEnd::HttpStatus => http_status
|
16
|
+
http_status
|
17
|
+
else
|
18
|
+
render
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Overrides component's render to add the concept of a layout.
|
23
|
+
# A layout is an HTML/ERB file that will contain this page's contents.
|
24
|
+
def render
|
25
|
+
Brut.container.layout_locator.locate(self.layout).
|
26
|
+
then { |layout_erb_file| Brut::FrontEnd::Template.new(layout_erb_file)
|
27
|
+
} => layout_template
|
28
|
+
|
29
|
+
Brut.container.page_locator.locate(self.template_name).
|
30
|
+
then { |erb_file| Brut::FrontEnd::Template.new(erb_file)
|
31
|
+
} => template
|
32
|
+
|
33
|
+
layout_template.render_template(self) do
|
34
|
+
template.render_template(self).html_safe!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.page_name = self.name
|
39
|
+
def page_name = self.class.page_name
|
40
|
+
def component_name = raise Brut::Framework::Errors::Bug,"#{self.class} is not a component"
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def template_name = RichString.new(self.class.name).underscorized.to_s.gsub(/^pages\//,"")
|
45
|
+
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Brut::FrontEnd::RequestContext
|
2
|
+
def initialize(env:,session:,flash:,xhr:,body:)
|
3
|
+
@hash = {
|
4
|
+
env:,
|
5
|
+
session:,
|
6
|
+
flash:,
|
7
|
+
xhr:,
|
8
|
+
body:,
|
9
|
+
csrf_token: Rack::Protection::AuthenticityToken.token(env["rack.session"]),
|
10
|
+
clock: Clock.new(session.timezone_from_browser),
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def []=(key,value)
|
16
|
+
key = key.to_sym
|
17
|
+
@hash[key] = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key)
|
21
|
+
if self.key?(key)
|
22
|
+
value = self[key]
|
23
|
+
if value
|
24
|
+
return value
|
25
|
+
else
|
26
|
+
raise ArgumentError,"No key '#{key}' in #{self.class}"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise ArgumentError,"Key '#{key}' is nil in #{self.class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](key)
|
34
|
+
@hash[key.to_sym]
|
35
|
+
end
|
36
|
+
|
37
|
+
def key?(key)
|
38
|
+
@hash.key?(key.to_sym)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a hash suitable to passing into this class' constructor.
|
42
|
+
def as_constructor_args(klass, request_params:)
|
43
|
+
args_for_method(method: klass.instance_method(:initialize), request_params:, form: nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
def as_method_args(object, method_name, request_params:,form:)
|
47
|
+
args_for_method(method: object.method(method_name), request_params:, form:)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def args_for_method(method:, request_params:, form: )
|
53
|
+
args = {}
|
54
|
+
method.parameters.each do |(type,name)|
|
55
|
+
|
56
|
+
if name.to_s == "**" || name.to_s == "*"
|
57
|
+
raise ArgumentError,"#{method.class}##{method.name} accepts '#{name}' and not keyword args. Define it in your class to accept the keyword arguments your method needs"
|
58
|
+
end
|
59
|
+
if ![ :key,:keyreq ].include?(type)
|
60
|
+
raise ArgumentError,"#{name} is not a keyword arg, but is a #{type}"
|
61
|
+
end
|
62
|
+
|
63
|
+
if self.key?(name)
|
64
|
+
args[name] = self[name]
|
65
|
+
elsif !form.nil? && name == :form
|
66
|
+
args[name] = form
|
67
|
+
elsif !request_params.nil? && (request_params[name.to_s] || request_params[name.to_sym])
|
68
|
+
args[name] = request_params[name.to_s] || request_params[name.to_sym]
|
69
|
+
elsif type == :keyreq
|
70
|
+
request_params_message = if request_params.nil?
|
71
|
+
"no request params provied"
|
72
|
+
else
|
73
|
+
"request_params: #{request_params.keys.map(&:to_s).join(", ")}"
|
74
|
+
end
|
75
|
+
raise ArgumentError,"#{method} argument '#{name}' is required, but there is no value in the current request context (keys: #{@hash.keys.map(&:to_s).join(", ")}, #{request_params_message}, form: #{form.class}). Either set this value in the request context or set a default value in the initializer"
|
76
|
+
else
|
77
|
+
# this keyword arg has a default value which will be used
|
78
|
+
end
|
79
|
+
end
|
80
|
+
args
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Brut::FrontEnd
|
2
|
+
class RouteHook
|
3
|
+
include Brut::FrontEnd::HandlingResults
|
4
|
+
# Return this to continue the hook
|
5
|
+
def continue = true
|
6
|
+
end
|
7
|
+
|
8
|
+
module RouteHooks
|
9
|
+
autoload(:LocaleDetection, "brut/front_end/route_hooks/locale_detection")
|
10
|
+
autoload(:SetupRequestContext, "brut/front_end/route_hooks/setup_request_context")
|
11
|
+
autoload(:AgeFlash, "brut/front_end/route_hooks/age_flash")
|
12
|
+
autoload(:CSPNoInlineStylesOrScripts,"brut/front_end/route_hooks/csp_no_inline_styles_or_scripts")
|
13
|
+
autoload(:CSPNoInlineScripts,"brut/front_end/route_hooks/csp_no_inline_scripts")
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Brut::FrontEnd::RouteHooks::CSPNoInlineScripts < Brut::FrontEnd::RouteHook
|
2
|
+
def after(response:)
|
3
|
+
response.headers["Content-Security-Policy"] = header_value
|
4
|
+
continue
|
5
|
+
end
|
6
|
+
|
7
|
+
def header_value
|
8
|
+
[
|
9
|
+
"default-src 'self'",
|
10
|
+
"script-src-elem 'self'",
|
11
|
+
"script-src-attr 'none'",
|
12
|
+
"style-src-elem 'self'",
|
13
|
+
"style-src-attr 'self'",
|
14
|
+
].join("; ")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts < Brut::FrontEnd::RouteHook
|
2
|
+
def after(response:)
|
3
|
+
response.headers["Content-Security-Policy"] = header_value
|
4
|
+
continue
|
5
|
+
end
|
6
|
+
|
7
|
+
def header_value
|
8
|
+
[
|
9
|
+
"default-src 'self'",
|
10
|
+
"script-src-elem 'self'",
|
11
|
+
"script-src-attr 'none'",
|
12
|
+
"style-src-elem 'self'",
|
13
|
+
"style-src-attr 'none'",
|
14
|
+
].join("; ")
|
15
|
+
end
|
16
|
+
|
17
|
+
class ReportOnly < Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts
|
18
|
+
def after(response:,request:)
|
19
|
+
csp_reporting_path = uri(Brut::FrontEnd::Handlers::CspReportingHandler.routing,request:)
|
20
|
+
reporting_directives = "report-to csp_reporting;report-uri #{csp_reporting_path}"
|
21
|
+
|
22
|
+
response.headers["Content-Security-Policy-Report-Only"] = header_value + ";" + reporting_directives
|
23
|
+
response.headers["Reporting-Endpoints"] = "csp_reporting='#{csp_reporting_path}'"
|
24
|
+
|
25
|
+
continue
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def uri(path,request:)
|
32
|
+
# Adapted from Sinatra's innards
|
33
|
+
host = "http#{'s' if request.secure?}://"
|
34
|
+
if request.forwarded? || (request.port != (request.secure? ? 443 : 80))
|
35
|
+
host << request.host_with_port
|
36
|
+
else
|
37
|
+
host << request.host
|
38
|
+
end
|
39
|
+
uri_parts = [
|
40
|
+
host,
|
41
|
+
request.script_name.to_s,
|
42
|
+
path,
|
43
|
+
]
|
44
|
+
File.join(uri_parts)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Brut::FrontEnd::RouteHooks::LocaleDetection < Brut::FrontEnd::RouteHook
|
2
|
+
def before(app_session:,env:)
|
3
|
+
http_accept_language = Brut::I18n::HTTPAcceptLanguage.from_header(env["HTTP_ACCEPT_LANGUAGE"])
|
4
|
+
if !app_session.http_accept_language.known?
|
5
|
+
app_session.http_accept_language = http_accept_language
|
6
|
+
end
|
7
|
+
best_locale = nil
|
8
|
+
app_session.http_accept_language.weighted_locales.each do |weighted_locale|
|
9
|
+
if ::I18n.available_locales.include?(weighted_locale.locale.to_sym)
|
10
|
+
best_locale = weighted_locale.locale.to_sym
|
11
|
+
break
|
12
|
+
elsif ::I18n.available_locales.include?(weighted_locale.primary_only.locale.to_sym)
|
13
|
+
best_locale = weighted_locale.primary_only.locale.to_sym
|
14
|
+
break
|
15
|
+
end
|
16
|
+
end
|
17
|
+
if best_locale
|
18
|
+
::I18n.locale = best_locale
|
19
|
+
else
|
20
|
+
SemanticLogger["Brut"].warn("None of the user's locales are available: #{app_session.http_accept_language}")
|
21
|
+
end
|
22
|
+
continue
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Brut::FrontEnd::RouteHooks::SetupRequestContext < Brut::FrontEnd::RouteHook
|
2
|
+
def before(app_session:,request:,env:)
|
3
|
+
flash = app_session.flash
|
4
|
+
app_session[:_flash] ||= flash
|
5
|
+
Thread.current.thread_variable_set(
|
6
|
+
:request_context,
|
7
|
+
Brut::FrontEnd::RequestContext.new(env:,session:app_session,flash:,xhr: request.xhr?,body: request.body)
|
8
|
+
)
|
9
|
+
continue
|
10
|
+
end
|
11
|
+
end
|