inline_forms 8.1.55 → 8.1.57
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -0
- data/app/controllers/concerns/file_slots_concern.rb +6 -0
- data/app/controllers/inline_forms_controller.rb +88 -30
- data/app/helpers/inline_forms_file_trash_helper.rb +10 -0
- data/app/models/concerns/inline_forms/stored_files.rb +5 -1
- data/app/models/inline_forms/trashed_file.rb +16 -2
- data/app/views/devise/sessions/_form.html.erb +2 -2
- data/app/views/inline_forms/_list.html.erb +24 -7
- data/app/views/inline_forms/_versions.html.erb +1 -1
- data/app/views/inline_forms/_versions_file_entry.html.erb +1 -1
- data/app/views/inline_forms/_versions_list.html.erb +14 -12
- data/app/views/layouts/application.html.erb +10 -3
- data/app/views/layouts/devise.html.erb +1 -1
- data/app/views/layouts/inline_forms.html.erb +9 -2
- data/lib/inline_forms/form_elements/file_field_helper.rb +3 -1
- data/lib/inline_forms/form_elements/info_list_helper.rb +3 -1
- data/lib/inline_forms/gem_files.rb +3 -2
- data/lib/inline_forms/version.rb +1 -1
- data/lib/locales/inline_forms.en.yml +22 -1
- data/lib/locales/inline_forms.nl.yml +25 -1
- data/test/dummy/app/controllers/application_controller.rb +5 -2
- data/test/dummy/app/controllers/daily_reports_controller.rb +4 -0
- data/test/dummy/app/models/daily_report.rb +21 -0
- data/test/dummy/app/models/machine.rb +1 -0
- data/test/dummy/config/application.rb +11 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/gem_package_modes_test.rb +1 -1
- data/test/integration/csp_nonce_test.rb +50 -0
- data/test/integration/file_slots_test.rb +91 -0
- data/test/integration/html_refusal_test.rb +152 -0
- data/test/integration/list_params_test.rb +200 -0
- data/test/integration/locale_test.rb +113 -0
- data/test/integration/presentation_escaping_test.rb +80 -0
- data/test/integration_test_helper.rb +11 -0
- metadata +15 -2
- data/app/views/inline_forms/_flash.html.erb +0 -13
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# Request params that end up in a list's markup.
|
|
6
|
+
#
|
|
7
|
+
# * parent_class / parent_id: _list built the nested frame as
|
|
8
|
+
# `raw "<turbo-frame id=\"#{update_span}\" …>"` with update_span made from
|
|
9
|
+
# params[:parent_id], and constantized params[:parent_class]; nothing found
|
|
10
|
+
# the parent before the render, so a crafted parent_id was reflected into
|
|
11
|
+
# the page. The controller now refuses malformed parent params (400) in
|
|
12
|
+
# index/new/create/show, and the frame is built with tag.turbo_frame.
|
|
13
|
+
# * pagination: the page links carry every filter of the query string, not
|
|
14
|
+
# just `search`.
|
|
15
|
+
#
|
|
16
|
+
# Part and DailyReport belong to Machine; Widget is a top-level list
|
|
17
|
+
# (per_page 7, see ApplicationRecord).
|
|
18
|
+
class ListParamsTest < InlineFormsIntegrationTestCase
|
|
19
|
+
PAYLOAD = %("><img src=x onerror=alert(1)>)
|
|
20
|
+
|
|
21
|
+
setup do
|
|
22
|
+
@machine = Machine.create!(name: "Press")
|
|
23
|
+
@part = Part.create!(name: "Gear", machine: @machine)
|
|
24
|
+
@list_frame = "machine_#{@machine.id}_parts_list"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def assert_refused_without_payload
|
|
28
|
+
assert_response :bad_request
|
|
29
|
+
assert controller.instance_variable_defined?(:@_authorized),
|
|
30
|
+
"the refusal runs after authorization (check_authorization stays satisfied)"
|
|
31
|
+
refute_includes response.body, "<img"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def nested(parent_id: @machine.id, parent_class: "Machine", update: @list_frame)
|
|
35
|
+
{ parent_class: parent_class, parent_id: parent_id, update: update, ul_needed: 1 }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ---- malformed parent params --------------------------------------------
|
|
39
|
+
|
|
40
|
+
test "a crafted parent_id on the nested list is a 400 and never rendered" do
|
|
41
|
+
get parts_path(nested(parent_id: PAYLOAD, update: "x")), headers: frame_headers("x")
|
|
42
|
+
assert_refused_without_payload
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
test "a crafted parent_id is refused on a full-page visit too" do
|
|
46
|
+
get parts_path(nested(parent_id: PAYLOAD, update: "x"))
|
|
47
|
+
assert_refused_without_payload
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test "a parent_class that is not an association of the model is a 400" do
|
|
51
|
+
%w[Widget Kernel machine Machines ActiveRecord::Base].each do |parent_class|
|
|
52
|
+
get parts_path(nested(parent_class: parent_class)), headers: frame_headers(@list_frame)
|
|
53
|
+
assert_response :bad_request, "parent_class=#{parent_class}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "a non-integer parent_id is a 400, with or without parent_class" do
|
|
58
|
+
[ "1 OR 1=1", "-1", "1.5", "" ].each do |parent_id|
|
|
59
|
+
get parts_path(nested(parent_id: parent_id)), headers: frame_headers(@list_frame)
|
|
60
|
+
assert_response :bad_request, "parent_id=#{parent_id.inspect}"
|
|
61
|
+
end
|
|
62
|
+
get parts_path(parent_id: PAYLOAD, update: "x")
|
|
63
|
+
assert_refused_without_payload
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test "new with crafted parent params is a 400" do
|
|
67
|
+
get new_part_path(nested(parent_id: PAYLOAD, update: "machine_#{@machine.id}_parts")),
|
|
68
|
+
headers: frame_headers("machine_#{@machine.id}_parts")
|
|
69
|
+
assert_refused_without_payload
|
|
70
|
+
|
|
71
|
+
get new_part_path(nested(parent_class: "Widget", update: "machine_#{@machine.id}_parts")),
|
|
72
|
+
headers: frame_headers("machine_#{@machine.id}_parts")
|
|
73
|
+
assert_response :bad_request
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
test "create with crafted parent params is a 400 and creates nothing" do
|
|
77
|
+
frame = "machine_#{@machine.id}_parts"
|
|
78
|
+
assert_no_difference -> { Part.count } do
|
|
79
|
+
post parts_path(nested(parent_id: PAYLOAD, update: frame)), params: { name: "Bolt" }, headers: frame_headers(frame)
|
|
80
|
+
assert_refused_without_payload
|
|
81
|
+
post parts_path(nested(parent_class: "Widget", update: frame)), params: { name: "Bolt" }, headers: frame_headers(frame)
|
|
82
|
+
assert_response :bad_request
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test "show with a crafted parent_id is a 400" do
|
|
87
|
+
row = "machine_#{@machine.id}_part_#{@part.id}"
|
|
88
|
+
get part_path(@part, update: row, parent_class: "Machine", parent_id: PAYLOAD), headers: frame_headers(row)
|
|
89
|
+
assert_refused_without_payload
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# ---- well-formed nested lists still work --------------------------------
|
|
93
|
+
|
|
94
|
+
test "a well-formed nested list renders its frame and rows" do
|
|
95
|
+
get parts_path(nested), headers: frame_headers(@list_frame)
|
|
96
|
+
|
|
97
|
+
assert_response :success
|
|
98
|
+
assert_includes response.body, %(<turbo-frame id="#{@list_frame}" class="list_container">)
|
|
99
|
+
assert_includes response.body, "Gear"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
test "well-formed nested new and create still work" do
|
|
103
|
+
frame = "machine_#{@machine.id}_parts"
|
|
104
|
+
get new_part_path(nested(update: frame)), headers: frame_headers(frame)
|
|
105
|
+
assert_response :success
|
|
106
|
+
|
|
107
|
+
assert_difference -> { @machine.parts.count }, 1 do
|
|
108
|
+
post parts_path(nested(update: frame)), params: { name: "Bolt" }, headers: frame_headers(frame)
|
|
109
|
+
end
|
|
110
|
+
assert_response :success
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
test "the nested list of a not_accessible_through_html? child still renders" do
|
|
114
|
+
DailyReport.create!(name: "Monday", machine: @machine)
|
|
115
|
+
frame = "machine_#{@machine.id}_daily_reports_list"
|
|
116
|
+
get daily_reports_path(parent_class: "Machine", parent_id: @machine.id, update: frame, ul_needed: 1),
|
|
117
|
+
headers: frame_headers(frame)
|
|
118
|
+
|
|
119
|
+
assert_response :success
|
|
120
|
+
assert_includes response.body, "Monday"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
test "the list frame id is attribute-escaped by the partial itself" do
|
|
124
|
+
html = PartsController.render(
|
|
125
|
+
partial: "inline_forms/list",
|
|
126
|
+
assigns: { Klass: Part, parent_class: "Machine", parent_id: PAYLOAD, ul_needed: "1",
|
|
127
|
+
objects: Part.where(id: @part.id).paginate(page: 1) }
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
refute_includes html, "<img"
|
|
131
|
+
assert_includes html, %(<turbo-frame id="machine_"><img src=x onerror=alert(1)>_parts_list" class="list_container">)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# ---- pagination keeps the filters ---------------------------------------
|
|
135
|
+
|
|
136
|
+
test "page links of a filtered list carry every filter" do
|
|
137
|
+
9.times { |i| Widget.create!(name: "Match #{i}") }
|
|
138
|
+
Widget.create!(name: "Other")
|
|
139
|
+
|
|
140
|
+
get widgets_path(search: "Match", sorteer: "name", first_name: "An", group: { id: 3 }, status: { id: 4 },
|
|
141
|
+
update: "widgets_list"),
|
|
142
|
+
headers: frame_headers("widgets_list")
|
|
143
|
+
|
|
144
|
+
assert_response :success
|
|
145
|
+
page_link = response.body[/href="([^"]*page=2[^"]*)"/, 1]
|
|
146
|
+
assert page_link, "a link to page 2"
|
|
147
|
+
query = Rack::Utils.parse_nested_query(URI.parse(CGI.unescapeHTML(page_link)).query)
|
|
148
|
+
assert_equal "Match", query["search"]
|
|
149
|
+
assert_equal "name", query["sorteer"]
|
|
150
|
+
assert_equal "An", query["first_name"]
|
|
151
|
+
assert_equal({ "id" => "3" }, query["group"])
|
|
152
|
+
assert_equal({ "id" => "4" }, query["status"])
|
|
153
|
+
assert_equal "widgets_list", query["update"]
|
|
154
|
+
assert_equal "2", query["page"]
|
|
155
|
+
|
|
156
|
+
get CGI.unescapeHTML(page_link), headers: frame_headers("widgets_list")
|
|
157
|
+
assert_response :success
|
|
158
|
+
back = response.body[/href="([^"]*page=1[^"]*)"/, 1]
|
|
159
|
+
assert back, "a link back to page 1"
|
|
160
|
+
back_query = Rack::Utils.parse_nested_query(URI.parse(CGI.unescapeHTML(back)).query)
|
|
161
|
+
assert_equal "Match", back_query["search"]
|
|
162
|
+
assert_equal({ "id" => "3" }, back_query["group"])
|
|
163
|
+
refute_includes response.body, "Other"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
test "page links of a list rendered after create keep the request's filters" do
|
|
167
|
+
9.times { |i| Machine.create!(name: "Match #{i}") }
|
|
168
|
+
|
|
169
|
+
post machines_path(update: "machines_list", search: "Match", sorteer: "name"),
|
|
170
|
+
params: { name: "Match 9" }, headers: frame_headers("machines_list")
|
|
171
|
+
|
|
172
|
+
assert_response :success
|
|
173
|
+
page_link = response.body[/href="([^"]*page=2[^"]*)"/, 1]
|
|
174
|
+
assert page_link, "a link to page 2"
|
|
175
|
+
query = Rack::Utils.parse_nested_query(URI.parse(CGI.unescapeHTML(page_link)).query)
|
|
176
|
+
assert_equal "Match", query["search"]
|
|
177
|
+
assert_equal "name", query["sorteer"]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# url_for options in the query string must not shape the page links.
|
|
181
|
+
# will_paginate leaves out script_name / original_script_name; merging
|
|
182
|
+
# request.query_parameters into the links bypassed that, so the payload
|
|
183
|
+
# became the start of every page link's href, and controller= raised.
|
|
184
|
+
test "url options in the query string never reach the page links" do
|
|
185
|
+
9.times { |i| Widget.create!(name: "W #{i}") }
|
|
186
|
+
|
|
187
|
+
%w[script_name original_script_name].each do |option|
|
|
188
|
+
get "/widgets?update=widgets_list&#{option}=javascript:alert(1)//", headers: frame_headers("widgets_list")
|
|
189
|
+
assert_response :success
|
|
190
|
+
page_link = response.body[/href="([^"]*page=2[^"]*)"/, 1]
|
|
191
|
+
assert page_link, "a link to page 2"
|
|
192
|
+
assert page_link.start_with?("/widgets?"), "#{option} shaped the page link: #{page_link}"
|
|
193
|
+
refute_includes response.body, "javascript:alert"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
get "/widgets?update=widgets_list&controller=widgets_x", headers: frame_headers("widgets_list")
|
|
197
|
+
assert_response :success
|
|
198
|
+
assert_match %r{href="/widgets\?[^"]*page=2}, response.body
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# The gem's own strings resolve in en and nl, also under
|
|
6
|
+
# raise_on_missing_translations (StProject switched to :nl with :strict):
|
|
7
|
+
# the create flash (`success`, misspelled `succes` in both locale files), the
|
|
8
|
+
# versions panel (was hard-coded English), the nl view keys that were missing
|
|
9
|
+
# (restore, list_versions, close_versions_list, undelete), localized version
|
|
10
|
+
# timestamps, the Devise layout's lang and the sign-in button.
|
|
11
|
+
class LocaleTest < InlineFormsIntegrationTestCase
|
|
12
|
+
KEYS = %w[
|
|
13
|
+
success
|
|
14
|
+
inline_forms.view.restore inline_forms.view.list_versions inline_forms.view.close_versions_list
|
|
15
|
+
inline_forms.view.undelete inline_forms.view.versions inline_forms.view.versions_event
|
|
16
|
+
inline_forms.view.versions_when inline_forms.view.versions_done_by inline_forms.view.versions_changeset
|
|
17
|
+
inline_forms.view.versions_old_value inline_forms.view.versions_new_value inline_forms.view.versions_empty
|
|
18
|
+
inline_forms.view.versions_rich_text inline_forms.view.version_events.create
|
|
19
|
+
inline_forms.view.version_events.update inline_forms.view.version_events.destroy
|
|
20
|
+
inline_forms.devise.login common.more common.logout
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
setup do
|
|
24
|
+
@machine = Machine.create!(name: "Press")
|
|
25
|
+
@machine.update!(name: "Press 2")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
teardown do
|
|
29
|
+
I18n.backend.reload!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Every translation the request looks up must exist: a missing one raises,
|
|
33
|
+
# in the views and in the controller (create's flash).
|
|
34
|
+
def strictly(locale)
|
|
35
|
+
original_view = ActionView::Helpers::TranslationHelper.raise_on_missing_translations
|
|
36
|
+
original_handler = I18n.exception_handler
|
|
37
|
+
original_default = I18n.default_locale
|
|
38
|
+
ActionView::Helpers::TranslationHelper.raise_on_missing_translations = true
|
|
39
|
+
I18n.exception_handler = ->(exception, *) { raise exception.respond_to?(:to_exception) ? exception.to_exception : exception }
|
|
40
|
+
# with_locale first: it restores the locale it found, so the thread's
|
|
41
|
+
# locale must not be read after default_locale changed (it would then
|
|
42
|
+
# restore :nl for every later test).
|
|
43
|
+
I18n.with_locale(locale) do
|
|
44
|
+
I18n.default_locale = locale
|
|
45
|
+
yield
|
|
46
|
+
end
|
|
47
|
+
ensure
|
|
48
|
+
I18n.default_locale = original_default
|
|
49
|
+
ActionView::Helpers::TranslationHelper.raise_on_missing_translations = original_view
|
|
50
|
+
I18n.exception_handler = original_handler
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
test "the gem's keys exist in en and nl, and the misspelled succes is gone" do
|
|
54
|
+
%i[en nl].each do |locale|
|
|
55
|
+
KEYS.each { |key| assert I18n.exists?(key, locale), "#{locale}.#{key}" }
|
|
56
|
+
refute I18n.exists?("succes", locale), "#{locale}.succes"
|
|
57
|
+
end
|
|
58
|
+
assert_equal "Machine created.", I18n.t("success", message: "Machine", locale: :en)
|
|
59
|
+
assert_equal "Machine is aangemaakt.", I18n.t("success", message: "Machine", locale: :nl)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
test "create's success flash translates in nl" do
|
|
63
|
+
# The model top bar shows t(controller_name): host-supplied, like
|
|
64
|
+
# StProject's `clients: cliënten`.
|
|
65
|
+
I18n.backend.store_translations(:nl, machines: "machines")
|
|
66
|
+
strictly(:nl) do
|
|
67
|
+
post machines_path(update: "machines_list"), params: { name: "Lathe" }, headers: frame_headers("machines_list")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
assert_response :success
|
|
71
|
+
assert_equal "Machine is aangemaakt.", controller.flash[:success]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
test "the versions panel and list are translated, with localized timestamps" do
|
|
75
|
+
row = "machine_#{@machine.id}"
|
|
76
|
+
strictly(:nl) do
|
|
77
|
+
get machine_path(@machine, update: row), headers: frame_headers(row)
|
|
78
|
+
assert_response :success
|
|
79
|
+
assert_includes response.body, "Versies (2)"
|
|
80
|
+
|
|
81
|
+
get list_versions_machine_path(@machine, update: row), headers: frame_headers(row)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
assert_response :success
|
|
85
|
+
body = response.body
|
|
86
|
+
%w[Versies Gebeurtenis Wanneer Door Wijzigingen aangemaakt gewijzigd terugzetten].each do |text|
|
|
87
|
+
assert_includes body, text
|
|
88
|
+
end
|
|
89
|
+
refute_includes body, "<strong>Event</strong>"
|
|
90
|
+
version = @machine.versions.last
|
|
91
|
+
assert_includes body, ERB::Util.h(I18n.l(version.created_at, format: :short, locale: :nl))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
test "the versions list in en keeps its English labels" do
|
|
95
|
+
row = "machine_#{@machine.id}"
|
|
96
|
+
strictly(:en) do
|
|
97
|
+
get list_versions_machine_path(@machine, update: row), headers: frame_headers(row)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
assert_response :success
|
|
101
|
+
assert_includes response.body, "Versions (2)"
|
|
102
|
+
assert_includes response.body, "<strong>Event</strong>"
|
|
103
|
+
assert_includes response.body, "old value"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
test "the devise and application layouts declare the page language" do
|
|
107
|
+
get widgets_path
|
|
108
|
+
I18n.with_locale(:nl) do
|
|
109
|
+
assert_includes controller.render_to_string(template: "stats/show", layout: "devise"), %(<html lang="nl">)
|
|
110
|
+
assert_includes controller.render_to_string(template: "stats/show", layout: "application"), %(<html lang="nl">)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# Model data that a helper concatenates into a string it marks html_safe must
|
|
6
|
+
# be escaped first. info_list_show did `out << item._presentation`, so a
|
|
7
|
+
# _presentation built from user input (StProject: User#name listed on a Role)
|
|
8
|
+
# ran as markup on every page that showed the list (stored XSS). Same audit:
|
|
9
|
+
# file_field_show (stored file name / url) and the rich-text changeset in the
|
|
10
|
+
# versions list (was `raw`).
|
|
11
|
+
class PresentationEscapingTest < InlineFormsIntegrationTestCase
|
|
12
|
+
MARKUP = "<img src=x onerror=alert(1)>"
|
|
13
|
+
ESCAPED = "<img src=x onerror=alert(1)>"
|
|
14
|
+
|
|
15
|
+
# A view context of a real request: helpers, can?, url helpers.
|
|
16
|
+
def view
|
|
17
|
+
@view ||= begin
|
|
18
|
+
get widgets_path
|
|
19
|
+
controller.view_context
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "info_list escapes each item's _presentation" do
|
|
24
|
+
machine = Machine.create!(name: "Press")
|
|
25
|
+
Part.create!(name: MARKUP, machine: machine)
|
|
26
|
+
Part.create!(name: "Gear", machine: machine)
|
|
27
|
+
|
|
28
|
+
html = view.info_list_show(machine, :parts)
|
|
29
|
+
|
|
30
|
+
refute_includes html, "<img"
|
|
31
|
+
assert_includes html, ESCAPED
|
|
32
|
+
assert_includes html, "Gear"
|
|
33
|
+
assert_includes html, "<div class='row", "the row markup itself is kept"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "file_field escapes the stored file name and url" do
|
|
37
|
+
widget = Widget.create!(name: "Manual")
|
|
38
|
+
stored = Struct.new(:url, :path) do
|
|
39
|
+
def present? = true
|
|
40
|
+
def to_s = url
|
|
41
|
+
end.new(%(/uploads/x'><img src=x onerror=alert(1)>.pdf), %(/uploads/#{MARKUP}.pdf))
|
|
42
|
+
widget.define_singleton_method(:manual) { stored }
|
|
43
|
+
|
|
44
|
+
html = view.file_field_show(widget, :manual)
|
|
45
|
+
|
|
46
|
+
refute_includes html, "<img"
|
|
47
|
+
assert_includes html, ESCAPED
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test "dropdown_with_other and the list rows escape _presentation" do
|
|
51
|
+
kind = Kind.create!(name: MARKUP)
|
|
52
|
+
widget = Widget.create!(name: MARKUP, kind: kind)
|
|
53
|
+
|
|
54
|
+
refute_includes view.dropdown_with_other_show(widget, :kind), "<img"
|
|
55
|
+
|
|
56
|
+
get widgets_path(update: "widgets_list"), headers: frame_headers("widgets_list")
|
|
57
|
+
assert_response :success
|
|
58
|
+
refute_includes response.body, "<img"
|
|
59
|
+
assert_includes response.body, ESCAPED
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
test "a rich-text changeset in the versions list is sanitized, not raw" do
|
|
63
|
+
machine = Machine.create!(name: "Press")
|
|
64
|
+
version = Struct.new(:id, :event, :changeset, :created_at, :whodunnit) do
|
|
65
|
+
def to_param = id.to_s
|
|
66
|
+
end.new(1, "update", { "body" => [ "<p>old</p><script>alert(1)</script>", "<p>new</p>#{MARKUP}" ] }, Time.current, nil)
|
|
67
|
+
context = view
|
|
68
|
+
context.instance_variable_set(:@object, machine)
|
|
69
|
+
context.define_singleton_method(:inline_forms_history_for) do |_object|
|
|
70
|
+
[ { version: version, kind: :rich_text, rich_text_name: "description", at: version.created_at } ]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
html = context.render(partial: "inline_forms/versions_list")
|
|
74
|
+
|
|
75
|
+
refute_includes html, "<script"
|
|
76
|
+
refute_includes html, "onerror"
|
|
77
|
+
assert_includes html, "<p>old</p>"
|
|
78
|
+
assert_includes html, "<p>new</p>"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -49,6 +49,15 @@ ActiveRecord::Schema.define do
|
|
|
49
49
|
end
|
|
50
50
|
add_index :parts, :machine_id
|
|
51
51
|
|
|
52
|
+
# DailyReport: a not_accessible_through_html? child of Machine (shaped like
|
|
53
|
+
# StProject's Dagrapportage). See test/integration/html_refusal_test.rb.
|
|
54
|
+
create_table :daily_reports, force: true do |t|
|
|
55
|
+
t.string :name
|
|
56
|
+
t.integer :machine_id
|
|
57
|
+
t.timestamps
|
|
58
|
+
end
|
|
59
|
+
add_index :daily_reports, :machine_id
|
|
60
|
+
|
|
52
61
|
# Gizmo intentionally has NO `pending_note` column: its attribute_list
|
|
53
62
|
# references one, modeling the inline_forms_addto pre-migrate window that
|
|
54
63
|
# InlineForms.attribute_pending_migration? gates. See
|
|
@@ -163,9 +172,11 @@ class InlineFormsIntegrationTestCase < ActionDispatch::IntegrationTest
|
|
|
163
172
|
|
|
164
173
|
setup do
|
|
165
174
|
Ability.restrictions = nil
|
|
175
|
+
ApplicationController.dummy_user_superadmin = true
|
|
166
176
|
Widget.delete_all
|
|
167
177
|
Kind.delete_all
|
|
168
178
|
Part.delete_all
|
|
179
|
+
DailyReport.delete_all
|
|
169
180
|
Machine.delete_all
|
|
170
181
|
Gizmo.delete_all
|
|
171
182
|
Dossier.delete_all
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inline_forms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.1.
|
|
4
|
+
version: 8.1.57
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ace Suares
|
|
@@ -153,7 +153,6 @@ files:
|
|
|
153
153
|
- app/views/inline_forms/_close.html.erb
|
|
154
154
|
- app/views/inline_forms/_edit.html.erb
|
|
155
155
|
- app/views/inline_forms/_file_slot.html.erb
|
|
156
|
-
- app/views/inline_forms/_flash.html.erb
|
|
157
156
|
- app/views/inline_forms/_generic_search.html.erb
|
|
158
157
|
- app/views/inline_forms/_header.html.erb
|
|
159
158
|
- app/views/inline_forms/_list.html.erb
|
|
@@ -282,6 +281,7 @@ files:
|
|
|
282
281
|
- test/archived_form_elements_test.rb
|
|
283
282
|
- test/attribute_list_test.rb
|
|
284
283
|
- test/dummy/app/controllers/application_controller.rb
|
|
284
|
+
- test/dummy/app/controllers/daily_reports_controller.rb
|
|
285
285
|
- test/dummy/app/controllers/documents_controller.rb
|
|
286
286
|
- test/dummy/app/controllers/dossiers_controller.rb
|
|
287
287
|
- test/dummy/app/controllers/gizmos_controller.rb
|
|
@@ -292,6 +292,7 @@ files:
|
|
|
292
292
|
- test/dummy/app/helpers/application_helper.rb
|
|
293
293
|
- test/dummy/app/models/ability.rb
|
|
294
294
|
- test/dummy/app/models/application_record.rb
|
|
295
|
+
- test/dummy/app/models/daily_report.rb
|
|
295
296
|
- test/dummy/app/models/document.rb
|
|
296
297
|
- test/dummy/app/models/dossier.rb
|
|
297
298
|
- test/dummy/app/models/gizmo.rb
|
|
@@ -312,15 +313,20 @@ files:
|
|
|
312
313
|
- test/inline_edit_polymorphic_path_test.rb
|
|
313
314
|
- test/inline_forms_addto_generator_test.rb
|
|
314
315
|
- test/inline_forms_generator_test.rb
|
|
316
|
+
- test/integration/csp_nonce_test.rb
|
|
315
317
|
- test/integration/field_authorization_test.rb
|
|
316
318
|
- test/integration/file_slots_test.rb
|
|
319
|
+
- test/integration/html_refusal_test.rb
|
|
317
320
|
- test/integration/inline_forms_crud_test.rb
|
|
318
321
|
- test/integration/layout_title_test.rb
|
|
319
322
|
- test/integration/legacy_elements_test.rb
|
|
323
|
+
- test/integration/list_params_test.rb
|
|
324
|
+
- test/integration/locale_test.rb
|
|
320
325
|
- test/integration/model_top_bar_test.rb
|
|
321
326
|
- test/integration/native_inputs_test.rb
|
|
322
327
|
- test/integration/open_after_create_test.rb
|
|
323
328
|
- test/integration/pending_migration_gate_test.rb
|
|
329
|
+
- test/integration/presentation_escaping_test.rb
|
|
324
330
|
- test/integration/revert_skipped_columns_test.rb
|
|
325
331
|
- test/integration/schema_batch_pipeline_test.rb
|
|
326
332
|
- test/integration/schema_edit_test.rb
|
|
@@ -363,6 +369,7 @@ test_files:
|
|
|
363
369
|
- test/archived_form_elements_test.rb
|
|
364
370
|
- test/attribute_list_test.rb
|
|
365
371
|
- test/dummy/app/controllers/application_controller.rb
|
|
372
|
+
- test/dummy/app/controllers/daily_reports_controller.rb
|
|
366
373
|
- test/dummy/app/controllers/documents_controller.rb
|
|
367
374
|
- test/dummy/app/controllers/dossiers_controller.rb
|
|
368
375
|
- test/dummy/app/controllers/gizmos_controller.rb
|
|
@@ -373,6 +380,7 @@ test_files:
|
|
|
373
380
|
- test/dummy/app/helpers/application_helper.rb
|
|
374
381
|
- test/dummy/app/models/ability.rb
|
|
375
382
|
- test/dummy/app/models/application_record.rb
|
|
383
|
+
- test/dummy/app/models/daily_report.rb
|
|
376
384
|
- test/dummy/app/models/document.rb
|
|
377
385
|
- test/dummy/app/models/dossier.rb
|
|
378
386
|
- test/dummy/app/models/gizmo.rb
|
|
@@ -393,15 +401,20 @@ test_files:
|
|
|
393
401
|
- test/inline_edit_polymorphic_path_test.rb
|
|
394
402
|
- test/inline_forms_addto_generator_test.rb
|
|
395
403
|
- test/inline_forms_generator_test.rb
|
|
404
|
+
- test/integration/csp_nonce_test.rb
|
|
396
405
|
- test/integration/field_authorization_test.rb
|
|
397
406
|
- test/integration/file_slots_test.rb
|
|
407
|
+
- test/integration/html_refusal_test.rb
|
|
398
408
|
- test/integration/inline_forms_crud_test.rb
|
|
399
409
|
- test/integration/layout_title_test.rb
|
|
400
410
|
- test/integration/legacy_elements_test.rb
|
|
411
|
+
- test/integration/list_params_test.rb
|
|
412
|
+
- test/integration/locale_test.rb
|
|
401
413
|
- test/integration/model_top_bar_test.rb
|
|
402
414
|
- test/integration/native_inputs_test.rb
|
|
403
415
|
- test/integration/open_after_create_test.rb
|
|
404
416
|
- test/integration/pending_migration_gate_test.rb
|
|
417
|
+
- test/integration/presentation_escaping_test.rb
|
|
405
418
|
- test/integration/revert_skipped_columns_test.rb
|
|
406
419
|
- test/integration/schema_batch_pipeline_test.rb
|
|
407
420
|
- test/integration/schema_edit_test.rb
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<% unless flash.empty? %>
|
|
2
|
-
<% identifier = Time.now.to_i.to_s %>
|
|
3
|
-
<div class="row" id="flash_<%= identifier %>">
|
|
4
|
-
<% flash.each do |key, value| %>
|
|
5
|
-
<%= value %>
|
|
6
|
-
<% end %>
|
|
7
|
-
<script>
|
|
8
|
-
$("#<%= "#{update_span}_auto_header" %>").ScrollTo();
|
|
9
|
-
$("#flash_<%= identifier %>").delay(1000).fadeToggle('2000');
|
|
10
|
-
</script>
|
|
11
|
-
</div>
|
|
12
|
-
<% end %>
|
|
13
|
-
|