brut 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/CODE_OF_CONDUCT.txt +99 -0
  4. data/Dockerfile.dx +32 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +133 -0
  7. data/LICENSE.txt +370 -0
  8. data/README.md +21 -0
  9. data/Rakefile +1 -0
  10. data/bin/bin_kit.rb +39 -0
  11. data/bin/rake +27 -0
  12. data/bin/setup +145 -0
  13. data/brut.gemspec +60 -0
  14. data/docker-compose.dx.yml +16 -0
  15. data/dx/build +26 -0
  16. data/dx/docker-compose.env +22 -0
  17. data/dx/dx.sh.lib +24 -0
  18. data/dx/exec +58 -0
  19. data/dx/prune +19 -0
  20. data/dx/setupkit.sh.lib +144 -0
  21. data/dx/show-help-in-app-container-then-wait.sh +38 -0
  22. data/dx/start +30 -0
  23. data/dx/stop +23 -0
  24. data/lib/brut/back_end/action.rb +3 -0
  25. data/lib/brut/back_end/result.rb +46 -0
  26. data/lib/brut/back_end/seed_data.rb +24 -0
  27. data/lib/brut/back_end/validator.rb +3 -0
  28. data/lib/brut/back_end/validators/form_validator.rb +37 -0
  29. data/lib/brut/cli/app.rb +130 -0
  30. data/lib/brut/cli/app_runner.rb +219 -0
  31. data/lib/brut/cli/apps/build_assets.rb +123 -0
  32. data/lib/brut/cli/apps/db.rb +279 -0
  33. data/lib/brut/cli/apps/scaffold.rb +256 -0
  34. data/lib/brut/cli/apps/test.rb +200 -0
  35. data/lib/brut/cli/command.rb +130 -0
  36. data/lib/brut/cli/error.rb +12 -0
  37. data/lib/brut/cli/execution_results.rb +81 -0
  38. data/lib/brut/cli/executor.rb +37 -0
  39. data/lib/brut/cli/options.rb +46 -0
  40. data/lib/brut/cli/output.rb +30 -0
  41. data/lib/brut/cli.rb +24 -0
  42. data/lib/brut/factory_bot.rb +20 -0
  43. data/lib/brut/framework/app.rb +55 -0
  44. data/lib/brut/framework/config.rb +415 -0
  45. data/lib/brut/framework/container.rb +190 -0
  46. data/lib/brut/framework/errors/abstract_method.rb +9 -0
  47. data/lib/brut/framework/errors/bug.rb +14 -0
  48. data/lib/brut/framework/errors/not_found.rb +10 -0
  49. data/lib/brut/framework/errors.rb +14 -0
  50. data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
  51. data/lib/brut/framework/mcp.rb +215 -0
  52. data/lib/brut/framework/project_environment.rb +18 -0
  53. data/lib/brut/framework.rb +13 -0
  54. data/lib/brut/front_end/asset_metadata.rb +76 -0
  55. data/lib/brut/front_end/component.rb +213 -0
  56. data/lib/brut/front_end/components/form_tag.rb +71 -0
  57. data/lib/brut/front_end/components/i18n_translations.rb +36 -0
  58. data/lib/brut/front_end/components/input.rb +13 -0
  59. data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
  60. data/lib/brut/front_end/components/inputs/select.rb +100 -0
  61. data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
  62. data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
  63. data/lib/brut/front_end/components/locale_detection.rb +25 -0
  64. data/lib/brut/front_end/components/page_identifier.rb +13 -0
  65. data/lib/brut/front_end/components/timestamp.rb +33 -0
  66. data/lib/brut/front_end/download.rb +23 -0
  67. data/lib/brut/front_end/flash.rb +57 -0
  68. data/lib/brut/front_end/form.rb +171 -0
  69. data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
  70. data/lib/brut/front_end/forms/input.rb +119 -0
  71. data/lib/brut/front_end/forms/input_definition.rb +100 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +36 -0
  73. data/lib/brut/front_end/handler.rb +48 -0
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
  75. data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
  76. data/lib/brut/front_end/handling_results.rb +14 -0
  77. data/lib/brut/front_end/http_method.rb +33 -0
  78. data/lib/brut/front_end/http_status.rb +16 -0
  79. data/lib/brut/front_end/middleware.rb +7 -0
  80. data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
  81. data/lib/brut/front_end/page.rb +47 -0
  82. data/lib/brut/front_end/request_context.rb +82 -0
  83. data/lib/brut/front_end/route_hook.rb +15 -0
  84. data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
  85. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
  86. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
  87. data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
  88. data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
  89. data/lib/brut/front_end/routing.rb +236 -0
  90. data/lib/brut/front_end/session.rb +56 -0
  91. data/lib/brut/front_end/template.rb +32 -0
  92. data/lib/brut/front_end/templates/block_filter.rb +60 -0
  93. data/lib/brut/front_end/templates/erb_engine.rb +26 -0
  94. data/lib/brut/front_end/templates/erb_parser.rb +84 -0
  95. data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
  96. data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
  97. data/lib/brut/i18n/base_methods.rb +168 -0
  98. data/lib/brut/i18n/for_cli.rb +4 -0
  99. data/lib/brut/i18n/for_html.rb +4 -0
  100. data/lib/brut/i18n/http_accept_language.rb +68 -0
  101. data/lib/brut/i18n.rb +6 -0
  102. data/lib/brut/instrumentation/basic.rb +66 -0
  103. data/lib/brut/instrumentation/event.rb +19 -0
  104. data/lib/brut/instrumentation/http_event.rb +5 -0
  105. data/lib/brut/instrumentation/subscriber.rb +41 -0
  106. data/lib/brut/instrumentation.rb +11 -0
  107. data/lib/brut/junk_drawer.rb +88 -0
  108. data/lib/brut/sinatra_helpers.rb +183 -0
  109. data/lib/brut/spec_support/component_support.rb +49 -0
  110. data/lib/brut/spec_support/flash_support.rb +7 -0
  111. data/lib/brut/spec_support/general_support.rb +18 -0
  112. data/lib/brut/spec_support/handler_support.rb +7 -0
  113. data/lib/brut/spec_support/matcher.rb +9 -0
  114. data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
  115. data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
  116. data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
  117. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
  118. data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
  119. data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
  120. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
  121. data/lib/brut/spec_support/session_support.rb +3 -0
  122. data/lib/brut/spec_support.rb +12 -0
  123. data/lib/brut/version.rb +3 -0
  124. data/lib/brut.rb +38 -0
  125. data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
  126. data/lib/sequel/extensions/brut_migrations.rb +98 -0
  127. data/lib/sequel/plugins/created_at.rb +14 -0
  128. data/lib/sequel/plugins/external_id.rb +45 -0
  129. data/lib/sequel/plugins/find_bang.rb +13 -0
  130. data/lib/sequel/plugins.rb +3 -0
  131. 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,7 @@
1
+ module Brut::FrontEnd
2
+ class Middleware
3
+ end
4
+ module Middlewares
5
+ autoload(:ReloadApp,"brut/front_end/middlewares/reload_app")
6
+ end
7
+ 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,8 @@
1
+ class Brut::FrontEnd::RouteHooks::AgeFlash < Brut::FrontEnd::RouteHook
2
+ def after(app_session:,request_context:)
3
+ flash = request_context[:flash]
4
+ flash.age!
5
+ app_session.flash = flash
6
+ continue
7
+ end
8
+ 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