brut 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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