inline_forms 8.1.53 → 8.1.56
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 +55 -0
- data/app/controllers/concerns/file_slots_concern.rb +6 -0
- data/app/controllers/inline_forms_controller.rb +39 -25
- data/app/views/devise/passwords/edit.html.erb +1 -1
- data/app/views/devise/passwords/new.html.erb +1 -1
- data/app/views/devise/shared/_header_and_errors.html.erb +9 -2
- data/lib/inline_forms/gem_files.rb +35 -2
- data/lib/inline_forms/version.rb +1 -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/routes.rb +5 -0
- data/test/gem_package_modes_test.rb +86 -0
- data/test/integration/html_refusal_test.rb +152 -0
- data/test/integration_test_helper.rb +11 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ecd64e62be7265a5989cc6012533cdac80dbd62f82ab8b009c17bd4c12343dd
|
|
4
|
+
data.tar.gz: fb677f7996a52a2a8d571f65264f22225a9ff9ed8c451657a2ac59c268d396d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5d4ed8bbb6569904df70ebbbd30b57c4847bb478088dfe70419363c58caf11d19dc6dfb5c67da2fbdaad8a85555c5b74b68847d19484f272d7c5fcbebcc30cf
|
|
7
|
+
data.tar.gz: a5b41fd5b309e7fd73bec6c652609f775493c1b3efdf977d3aed1503c6e6656d4e8d7e695bac99ca751b914bb2a47be64bdb244233820253d2426e87240515ee
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,61 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [8.1.56] - 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **HTML requests the engine refuses get an explicit answer instead of falling through to implicit rendering.** Several `InlineFormsController` actions registered `format.html` only behind a guard, or registered it with a block that rendered nothing on the refused path. Rails then rendered implicitly: `ActionController::MissingExactTemplate` (a 500 for browser requests), `UnknownFormat` (406) or a bare 204. That meant error pages and exception-notifier noise. StProject hit it as a 500 on `GET /dagrapportages` by a normal user: `Dagrapportage` is `not_accessible_through_html?` and only exists nested in a Client tab. Now:
|
|
12
|
+
- `index` of a `not_accessible_through_html?` model without `parent_class` → **404**. The resource has no top-level page.
|
|
13
|
+
- `new`, `create`, row `show`, `destroy`, `soft_delete`, `soft_restore` and `revert` without the frame context the gem needs (`update`, plus `parent_class` or a nested row frame for `not_accessible_through_html?` models) → **400**. The request is malformed for this UI rather than aimed at a missing resource, so 400 rather than 404. A guard that does not match the request is a refusal of the request.
|
|
14
|
+
- `destroy` by a user who may not hard-destroy (`destroy_permitted?`, i.e. not superadmin) → **403**, the same answer `revert` already gave. It used to render nothing.
|
|
15
|
+
- `create`, `destroy`, `soft_delete`, `soft_restore` and `revert` now refuse **before writing**. Before, `create` saved the record, and `destroy`/`revert` changed data, and only then failed to respond.
|
|
16
|
+
- The refusals happen after `load_and_authorize_resource` / `authorize!`, so a host's CanCan `check_authorization` stays satisfied. The working paths are unchanged, including the turbo_stream responses (open-after-create, revert), nested lists, forms and rows, and accessible models' lists and rows.
|
|
17
|
+
- **File-slot actions under `check_authorization`.** `file_slot_request_permitted?` answered 400 for an unlisted attribute before any `authorize!`, so a host with CanCan's `check_authorization` raised `CanCan::AuthorizationNotPerformed` (500) instead. It now authorizes the record first, then the attribute.
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
|
|
21
|
+
- `test/integration/html_refusal_test.rb`, using new dummy model `DailyReport` (a `not_accessible_through_html?` child of `Machine`):
|
|
22
|
+
- top-level index → 404; show, new and revert without a nested frame → 400; create → 400 with nothing created; destroy → 400 with the record kept;
|
|
23
|
+
- nested list, new, create, row, destroy and revert still work;
|
|
24
|
+
- an accessible model's list and row still render, and its new/create without `update` → 400;
|
|
25
|
+
- a non-superadmin destroy → 403;
|
|
26
|
+
- a refused file-slot request still counts as authorized.
|
|
27
|
+
- Every refusal also asserts that authorization ran.
|
|
28
|
+
|
|
29
|
+
### Lockstep
|
|
30
|
+
|
|
31
|
+
- validation_hints 8.1.56, inline_forms_installer 8.1.56, inline_forms_schema_edit 8.1.56.
|
|
32
|
+
|
|
33
|
+
## [8.1.55] - 2026-10-04
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Published gems were unreadable for other users.** RubyGems packages each file with its on-disk mode, and `gem install` restores it. The release checkout is group-only (`rw-rw----`), so every file in the published 8.1.53 gems was `0660`. After `gem install` only the installing user and group could read them: for anyone else `require "inline_forms"` failed silently (Bundler swallows that LoadError), so the app died at boot with `uninitialized constant InlineForms`. A Docker image that installs gems as root and runs as another user could not boot at all. Gem builds now package every file as `0644`, or `0755` when the owner may execute it, whatever the checkout's modes: `InlineFormsGemFiles` wraps `Gem::Package#add_files`, and all three gemspecs in this repo load it, so `gem build`, `rake build` and the CI full gate are all covered. validation_hints 8.1.55 has the same fix. 8.1.53 cannot be republished (RubyGems never accepts a version twice), so apps on 8.1.53 should move to 8.1.55.
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
- Gem: `test/gem_package_modes_test.rb`: group-only files on disk are packaged `0644`/`0755`, and the inline_forms, inline_forms_installer and inline_forms_schema_edit gemspecs build with every file world-readable.
|
|
42
|
+
|
|
43
|
+
### Lockstep
|
|
44
|
+
|
|
45
|
+
- validation_hints 8.1.55, inline_forms_installer 8.1.55, inline_forms_schema_edit 8.1.55.
|
|
46
|
+
|
|
47
|
+
## [8.1.54] - 2026-10-04
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **"Forgot your password?" and the password reset page raised.** Devise 5 removed `devise_error_messages!`, and the gem's `devise/passwords/new` and `devise/passwords/edit` still called it, so both pages failed with `undefined method 'devise_error_messages!'` in every generated app. They now render Devise's own `devise/shared/error_messages` partial (`render "devise/shared/error_messages", resource: resource`).
|
|
52
|
+
- **`devise/shared/_header_and_errors`** (not rendered by the gem's own views; kept for host apps that render it). It looked up a translation named after the app (`t application_name`, which renders a translation_missing span), called the removed `devise_error_messages!`, and looked up `inline_forms.devise.` when `@default_message` was unset. It now shows `t('application_name', default: application_name)` like the `inline_forms` layout's title, Devise's error list when the resource has errors, and the `inline_forms.devise.<@default_message>` text only when `@default_message` is set.
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
|
|
56
|
+
- Example app: `example_app_devise_passwords_test.rb`: the forgot-password and reset-password pages render, an unknown email re-renders the form with Devise's error list, and `_header_and_errors` shows the app name and the resource errors. The engine's dummy app has no Devise, so these tests live in the example app only.
|
|
57
|
+
|
|
58
|
+
### Lockstep
|
|
59
|
+
|
|
60
|
+
- validation_hints 8.1.54, inline_forms_installer 8.1.54, inline_forms_schema_edit 8.1.54.
|
|
61
|
+
|
|
7
62
|
## [8.1.53] - 2026-10-04
|
|
8
63
|
|
|
9
64
|
### Fixed
|
|
@@ -136,10 +136,16 @@ module FileSlotsConcern
|
|
|
136
136
|
# Validates the attribute (declared slot shown as :simple_file_field in the
|
|
137
137
|
# attribute list), then authorizes +action+ on it. Renders and returns false
|
|
138
138
|
# when refused.
|
|
139
|
+
#
|
|
140
|
+
# The record is authorized first (these actions skip
|
|
141
|
+
# load_and_authorize_resource): a 400 for an unlisted attribute must still
|
|
142
|
+
# count as an authorization check, or a host with CanCan's
|
|
143
|
+
# +check_authorization+ raises AuthorizationNotPerformed (500) instead.
|
|
139
144
|
def file_slot_request_permitted?(action)
|
|
140
145
|
@attribute = params[:attribute].to_s
|
|
141
146
|
@form_element = "simple_file_field"
|
|
142
147
|
@update_span = params[:update]
|
|
148
|
+
authorize!(action, @object) if cancan_enabled?
|
|
143
149
|
unless inline_forms_file_slot_listed?(@object, @attribute)
|
|
144
150
|
head :bad_request
|
|
145
151
|
return false
|
|
@@ -84,6 +84,11 @@ class InlineFormsController < ApplicationController
|
|
|
84
84
|
format.html do
|
|
85
85
|
if @parent_class.present?
|
|
86
86
|
render_nested_associated_list_html
|
|
87
|
+
else
|
|
88
|
+
# No top-level page for this resource. An explicit answer: an
|
|
89
|
+
# empty block would fall through to implicit rendering and raise
|
|
90
|
+
# MissingExactTemplate (500).
|
|
91
|
+
head :not_found
|
|
87
92
|
end
|
|
88
93
|
end
|
|
89
94
|
else
|
|
@@ -116,7 +121,7 @@ class InlineFormsController < ApplicationController
|
|
|
116
121
|
|
|
117
122
|
@object.inline_forms_attribute_list = @inline_forms_attribute_list if @inline_forms_attribute_list
|
|
118
123
|
respond_to do |format|
|
|
119
|
-
format.html { render_turbo_new
|
|
124
|
+
format.html { html_list_flow_allowed? ? render_turbo_new : head(:bad_request) }
|
|
120
125
|
end
|
|
121
126
|
end
|
|
122
127
|
|
|
@@ -139,6 +144,11 @@ class InlineFormsController < ApplicationController
|
|
|
139
144
|
def create
|
|
140
145
|
@object ||= @Klass.new
|
|
141
146
|
@update_span = params[:update]
|
|
147
|
+
@parent_class = params[:parent_class]
|
|
148
|
+
# Refuse before anything is written: the responses below only exist for
|
|
149
|
+
# a request from a list or nested-list frame.
|
|
150
|
+
return head :bad_request unless html_list_flow_allowed?
|
|
151
|
+
|
|
142
152
|
attributes = @inline_forms_attribute_list || @object.inline_forms_attribute_list
|
|
143
153
|
attributes.each do | attribute, form_element |
|
|
144
154
|
# Skip a row whose column is not migrated yet (model edited, migration
|
|
@@ -149,7 +159,6 @@ class InlineFormsController < ApplicationController
|
|
|
149
159
|
InlineForms.assert_plain_text_column!(object: @object, attribute: attribute, form_element: form_element)
|
|
150
160
|
send("#{form_element}_update", @object, attribute) unless form_element == :associated || (cancan_enabled? && cannot?(:read, @object, attribute))
|
|
151
161
|
end
|
|
152
|
-
@parent_class = params[:parent_class]
|
|
153
162
|
@parent_id = params[:parent_id]
|
|
154
163
|
# See #index for the order/search/parent-fk decomposition.
|
|
155
164
|
fk_conditions = nil
|
|
@@ -171,19 +180,17 @@ class InlineFormsController < ApplicationController
|
|
|
171
180
|
created_object = @object
|
|
172
181
|
@object = nil
|
|
173
182
|
respond_to do |format|
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
format.turbo_stream { render_created_row_open_streams(created_object) }
|
|
177
|
-
end
|
|
178
|
-
format.html { render_list_frame_after_save }
|
|
183
|
+
if open_created_row?(created_object, attributes)
|
|
184
|
+
format.turbo_stream { render_created_row_open_streams(created_object) }
|
|
179
185
|
end
|
|
186
|
+
format.html { render_list_frame_after_save }
|
|
180
187
|
end
|
|
181
188
|
else
|
|
182
189
|
flash.now[:header] = [ "Kan #{@object.class.to_s.underscore} niet aanmaken." ]
|
|
183
190
|
flash.now[:error] = @object.errors.to_a
|
|
184
191
|
respond_to do |format|
|
|
185
192
|
@object.inline_forms_attribute_list = attributes
|
|
186
|
-
format.html { render_turbo_new }
|
|
193
|
+
format.html { render_turbo_new }
|
|
187
194
|
end
|
|
188
195
|
end
|
|
189
196
|
end
|
|
@@ -257,10 +264,10 @@ class InlineFormsController < ApplicationController
|
|
|
257
264
|
if @attribute.nil?
|
|
258
265
|
respond_to do |format|
|
|
259
266
|
@attributes = @object.inline_forms_attribute_list
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
format.html do
|
|
268
|
+
next head(:bad_request) unless row_html_turbo_allowed?
|
|
269
|
+
|
|
270
|
+
render_row_turbo(close ? :close : :show)
|
|
264
271
|
end
|
|
265
272
|
end
|
|
266
273
|
else
|
|
@@ -274,9 +281,11 @@ class InlineFormsController < ApplicationController
|
|
|
274
281
|
def soft_delete
|
|
275
282
|
@update_span = params[:update]
|
|
276
283
|
@object = referenced_object
|
|
284
|
+
return head :bad_request unless row_html_turbo_allowed?
|
|
285
|
+
|
|
277
286
|
@object.soft_delete(current_user)
|
|
278
287
|
respond_to do |format|
|
|
279
|
-
format.html { render_row_turbo(:close) }
|
|
288
|
+
format.html { render_row_turbo(:close) }
|
|
280
289
|
end
|
|
281
290
|
end
|
|
282
291
|
|
|
@@ -284,9 +293,11 @@ class InlineFormsController < ApplicationController
|
|
|
284
293
|
def soft_restore
|
|
285
294
|
@update_span = params[:update]
|
|
286
295
|
@object = referenced_object
|
|
296
|
+
return head :bad_request unless row_html_turbo_allowed?
|
|
297
|
+
|
|
287
298
|
@object.soft_restore
|
|
288
299
|
respond_to do |format|
|
|
289
|
-
format.html { render_row_turbo(:close) }
|
|
300
|
+
format.html { render_row_turbo(:close) }
|
|
290
301
|
end
|
|
291
302
|
end
|
|
292
303
|
|
|
@@ -294,14 +305,17 @@ class InlineFormsController < ApplicationController
|
|
|
294
305
|
def destroy
|
|
295
306
|
@update_span = params[:update]
|
|
296
307
|
@object = referenced_object
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
# Both refusals answer before anything is destroyed (they used to render
|
|
309
|
+
# nothing and fall through to implicit rendering: a 500).
|
|
310
|
+
return head :forbidden unless destroy_permitted?
|
|
311
|
+
return head :bad_request unless row_html_turbo_allowed?
|
|
312
|
+
|
|
313
|
+
@object.destroy
|
|
314
|
+
# Capture after destroy: `.last` before destroy was the latest *update*
|
|
315
|
+
# (e.g. a plain_text_area edit), so undo reified the pre-edit state.
|
|
316
|
+
@undo_version = @object.versions.last
|
|
317
|
+
respond_to do |format|
|
|
318
|
+
format.html { render_row_turbo_destroyed }
|
|
305
319
|
end
|
|
306
320
|
end
|
|
307
321
|
|
|
@@ -327,6 +341,7 @@ class InlineFormsController < ApplicationController
|
|
|
327
341
|
# has the Devise+roles system, otherwise no extra gate beyond CanCan.
|
|
328
342
|
return head :forbidden unless destroy_permitted?
|
|
329
343
|
return head :not_found unless @parent
|
|
344
|
+
return head :bad_request unless row_html_turbo_allowed?
|
|
330
345
|
|
|
331
346
|
@object = reify_for_revert(@version)
|
|
332
347
|
# PaperTrail::Version#reify returns nil for `create` events because
|
|
@@ -354,8 +369,7 @@ class InlineFormsController < ApplicationController
|
|
|
354
369
|
@parent = item || @parent
|
|
355
370
|
return head :not_found unless @parent
|
|
356
371
|
end
|
|
357
|
-
return render_revert_response
|
|
358
|
-
return
|
|
372
|
+
return render_revert_response
|
|
359
373
|
end
|
|
360
374
|
if defined?(ActionText::RichText) && @object.is_a?(ActionText::RichText)
|
|
361
375
|
@rich_text_record = @object
|
|
@@ -374,7 +388,7 @@ class InlineFormsController < ApplicationController
|
|
|
374
388
|
end
|
|
375
389
|
@parent.reload
|
|
376
390
|
end
|
|
377
|
-
render_revert_response
|
|
391
|
+
render_revert_response
|
|
378
392
|
end
|
|
379
393
|
|
|
380
394
|
private
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
<%# Unused by the gem's own devise views; kept for host apps that render it. %>
|
|
2
|
+
<%# Same title source as layouts/inline_forms; Devise 5 dropped %>
|
|
3
|
+
<%# devise_error_messages!, so errors use Devise's own error_messages partial. %>
|
|
4
|
+
<div id="large_title"><%= t('application_name', default: application_name) %></div>
|
|
2
5
|
<div id="flash_message">
|
|
3
|
-
|
|
6
|
+
<% if resource.errors.any? %>
|
|
7
|
+
<%= render "devise/shared/error_messages", resource: resource %>
|
|
8
|
+
<% elsif @default_message.present? %>
|
|
9
|
+
<%= t("inline_forms.devise.#{@default_message}") %>
|
|
10
|
+
<% end %>
|
|
4
11
|
</div>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "rubygems/package"
|
|
5
|
+
|
|
3
6
|
module InlineFormsGemFiles
|
|
4
7
|
INSTALLER_FILE_PREFIXES = %w[
|
|
5
8
|
bin/inline_forms
|
|
@@ -24,8 +27,9 @@ module InlineFormsGemFiles
|
|
|
24
27
|
# the token leaked into `gem build` on the release machine. Exclude it hard,
|
|
25
28
|
# independent of any ignore configuration.
|
|
26
29
|
#
|
|
27
|
-
# Same for
|
|
28
|
-
#
|
|
30
|
+
# Same for Claude Code files: .claude/ (untracked, per checkout:
|
|
31
|
+
# launch.json, settings.local.json, worktrees) and CLAUDE.md (committed so
|
|
32
|
+
# worktrees see it, but repo instructions, not gem content).
|
|
29
33
|
EXCLUDED_FILE_PREFIXES = %w[
|
|
30
34
|
stuff/
|
|
31
35
|
.claude/
|
|
@@ -68,4 +72,33 @@ module InlineFormsGemFiles
|
|
|
68
72
|
include_installer ? !installer_file : installer_file
|
|
69
73
|
end
|
|
70
74
|
end
|
|
75
|
+
|
|
76
|
+
# Mode a file gets inside the .gem: 0755 if the owner may execute it,
|
|
77
|
+
# else 0644. RubyGems packages each file's on-disk mode (File.lstat) and
|
|
78
|
+
# `gem install` restores it, and this checkout is group-only (rw-rw----),
|
|
79
|
+
# so 8.1.53 shipped every file as 0660: only the installing user and group
|
|
80
|
+
# could read the installed gem, `require "inline_forms"` failed silently
|
|
81
|
+
# for anyone else (Bundler swallows the LoadError), and a Docker image that
|
|
82
|
+
# installs as root and runs as another user could not boot.
|
|
83
|
+
def packaged_mode(mode)
|
|
84
|
+
(mode & 0o100).zero? ? 0o644 : 0o755
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Wraps the tar writer Gem::Package#add_files writes to, so every file
|
|
88
|
+
# goes in with packaged_mode instead of its on-disk mode.
|
|
89
|
+
class ReadableTar < SimpleDelegator
|
|
90
|
+
def add_file_simple(name, mode, size, &block)
|
|
91
|
+
__getobj__.add_file_simple(name, InlineFormsGemFiles.packaged_mode(mode), size, &block)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module ReadablePackage
|
|
96
|
+
def add_files(tar)
|
|
97
|
+
super(ReadableTar.new(tar))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
71
100
|
end
|
|
101
|
+
|
|
102
|
+
# Loaded by every gemspec in this repo, so `gem build`, `rake build` and the
|
|
103
|
+
# CI full gate all package world-readable files whatever the checkout's modes.
|
|
104
|
+
Gem::Package.prepend(InlineFormsGemFiles::ReadablePackage) unless Gem::Package <= InlineFormsGemFiles::ReadablePackage
|
data/lib/inline_forms/version.rb
CHANGED
|
@@ -5,10 +5,13 @@ class ApplicationController < ActionController::Base
|
|
|
5
5
|
# create/new HTML responses) calls `current_user.name` and
|
|
6
6
|
# `current_user.role?`, so a nil current_user cannot render it. The stub is
|
|
7
7
|
# superadmin so `destroy_permitted?` allows hard destroy, matching the
|
|
8
|
-
# generated apps' test user.
|
|
8
|
+
# generated apps' test user. A test can switch that off with
|
|
9
|
+
# dummy_user_superadmin (reset in InlineFormsIntegrationTestCase#setup).
|
|
10
|
+
mattr_accessor :dummy_user_superadmin, default: true
|
|
11
|
+
|
|
9
12
|
DummyUser = Struct.new(:id, :name) do
|
|
10
13
|
def role?(role)
|
|
11
|
-
role.to_sym == :superadmin
|
|
14
|
+
role.to_sym == :superadmin && ApplicationController.dummy_user_superadmin
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Child of Machine that has no top-level HTML surface: it is only shown
|
|
4
|
+
# nested in its machine (like StProject's Dagrapportage inside a Client tab).
|
|
5
|
+
class DailyReport < ApplicationRecord
|
|
6
|
+
belongs_to :machine, optional: true
|
|
7
|
+
|
|
8
|
+
def self.not_accessible_through_html?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def _presentation
|
|
13
|
+
"#{name}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def inline_forms_attribute_list
|
|
17
|
+
@inline_forms_attribute_list ||= [
|
|
18
|
+
[ :name, :text_field ]
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
end
|
data/test/dummy/config/routes.rb
CHANGED
|
@@ -20,6 +20,11 @@ Rails.application.routes.draw do
|
|
|
20
20
|
get "list_versions", on: :member
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
resources :daily_reports do
|
|
24
|
+
post "revert", on: :member
|
|
25
|
+
get "list_versions", on: :member
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
resources :gizmos do
|
|
24
29
|
post "revert", on: :member
|
|
25
30
|
get "list_versions", on: :member
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
4
|
+
|
|
5
|
+
require "minitest/autorun"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "zlib"
|
|
8
|
+
require "inline_forms/gem_files"
|
|
9
|
+
|
|
10
|
+
# 8.1.53 shipped every file as 0660 (the checkout's group-only modes), so an
|
|
11
|
+
# installed gem was unreadable for anyone outside the installing user and
|
|
12
|
+
# group. Gem builds now package 0644 / 0755 whatever the on-disk modes.
|
|
13
|
+
class GemPackageModesTest < Minitest::Test
|
|
14
|
+
REPO_ROOT = File.expand_path("..", __dir__)
|
|
15
|
+
|
|
16
|
+
def packaged_modes(gem_path)
|
|
17
|
+
modes = {}
|
|
18
|
+
File.open(gem_path, "rb") do |io|
|
|
19
|
+
Gem::Package::TarReader.new(io).each do |entry|
|
|
20
|
+
next unless entry.full_name == "data.tar.gz"
|
|
21
|
+
|
|
22
|
+
Gem::Package::TarReader.new(Zlib::GzipReader.new(entry)).each do |file|
|
|
23
|
+
modes[file.full_name] = file.header.mode & 0o777
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
modes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build(spec, dir, gem_dir)
|
|
31
|
+
gem_path = File.join(gem_dir, "#{spec.full_name}.gem")
|
|
32
|
+
Dir.chdir(dir) do
|
|
33
|
+
package = Gem::Package.new(gem_path)
|
|
34
|
+
package.spec = spec
|
|
35
|
+
Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) { package.build(true) }
|
|
36
|
+
end
|
|
37
|
+
gem_path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_packaged_mode_keeps_only_the_owner_execute_bit
|
|
41
|
+
assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100660)
|
|
42
|
+
assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100600)
|
|
43
|
+
assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100770)
|
|
44
|
+
assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100700)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_group_only_files_are_packaged_world_readable
|
|
48
|
+
Dir.mktmpdir do |src|
|
|
49
|
+
Dir.mktmpdir do |out|
|
|
50
|
+
File.write(File.join(src, "plain.rb"), "# plain\n")
|
|
51
|
+
File.write(File.join(src, "tool"), "#!/bin/sh\n")
|
|
52
|
+
File.chmod(0o660, File.join(src, "plain.rb"))
|
|
53
|
+
File.chmod(0o700, File.join(src, "tool"))
|
|
54
|
+
|
|
55
|
+
spec = Gem::Specification.new do |s|
|
|
56
|
+
s.name = "modes_probe"
|
|
57
|
+
s.version = "0.0.1"
|
|
58
|
+
s.summary = "probe"
|
|
59
|
+
s.authors = [ "probe" ]
|
|
60
|
+
s.files = %w[plain.rb tool]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
modes = packaged_modes(build(spec, src, out))
|
|
64
|
+
|
|
65
|
+
assert_equal({ "plain.rb" => 0o644, "tool" => 0o755 }, modes)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_every_gemspec_in_the_repo_packages_world_readable_files
|
|
71
|
+
%w[inline_forms.gemspec inline_forms_installer.gemspec
|
|
72
|
+
inline_forms_schema_edit/inline_forms_schema_edit.gemspec].each do |gemspec|
|
|
73
|
+
path = File.join(REPO_ROOT, gemspec)
|
|
74
|
+
spec = Gem::Specification.load(path)
|
|
75
|
+
refute_nil spec, gemspec
|
|
76
|
+
|
|
77
|
+
Dir.mktmpdir do |out|
|
|
78
|
+
modes = packaged_modes(build(spec, File.dirname(path), out))
|
|
79
|
+
|
|
80
|
+
refute_empty modes, gemspec
|
|
81
|
+
unreadable = modes.reject { |_, mode| (mode & 0o444) == 0o444 }
|
|
82
|
+
assert_empty unreadable, "#{gemspec}: not world-readable in the .gem"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# Every HTML request the engine decides not to serve gets an explicit answer:
|
|
6
|
+
# a top-level list of a not_accessible_through_html? model is a 404, a
|
|
7
|
+
# new/create/show/destroy/revert without the frame context the gem needs is a
|
|
8
|
+
# 400. Before, the format.html block rendered nothing (or was not registered),
|
|
9
|
+
# so Rails fell through to implicit rendering: ActionController::
|
|
10
|
+
# MissingExactTemplate (500; StProject's GET /dagrapportages) or UnknownFormat
|
|
11
|
+
# (406). create, destroy and revert also refuse before writing anything.
|
|
12
|
+
#
|
|
13
|
+
# DailyReport is the not_accessible_through_html? child of Machine; Machine is
|
|
14
|
+
# a normal (accessible) model.
|
|
15
|
+
class HtmlRefusalTest < InlineFormsIntegrationTestCase
|
|
16
|
+
setup do
|
|
17
|
+
@machine = Machine.create!(name: "Press")
|
|
18
|
+
@report = DailyReport.create!(name: "Monday", machine: @machine)
|
|
19
|
+
@list_frame = "machine_#{@machine.id}_daily_reports"
|
|
20
|
+
@row_frame = "machine_#{@machine.id}_daily_report_#{@report.id}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def nested_params(update = @list_frame)
|
|
24
|
+
{ update: update, parent_class: "Machine", parent_id: @machine.id }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stream_headers(frame_id)
|
|
28
|
+
frame_headers(frame_id).merge("Accept" => "text/vnd.turbo-stream.html")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The refusal ran after authorization, so CanCan's check_authorization (an
|
|
32
|
+
# after_action in host apps) is satisfied.
|
|
33
|
+
def assert_refused(status)
|
|
34
|
+
assert_response status
|
|
35
|
+
assert controller.instance_variable_defined?(:@_authorized),
|
|
36
|
+
"the refused request must still count as authorized (check_authorization)"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ---- not_accessible_through_html? model, top level -----------------------
|
|
40
|
+
|
|
41
|
+
test "the top-level list of a not_accessible_through_html? model is a 404" do
|
|
42
|
+
get daily_reports_path
|
|
43
|
+
assert_refused :not_found
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "show without a nested row frame is a 400" do
|
|
47
|
+
get daily_report_path(@report)
|
|
48
|
+
assert_refused :bad_request
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "new without update and parent_class is a 400" do
|
|
52
|
+
get new_daily_report_path
|
|
53
|
+
assert_refused :bad_request
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test "create without update and parent_class is a 400 and creates nothing" do
|
|
57
|
+
assert_no_difference -> { DailyReport.count } do
|
|
58
|
+
post daily_reports_path, params: { name: "Tuesday" }
|
|
59
|
+
end
|
|
60
|
+
assert_refused :bad_request
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
test "destroy without a nested row frame is a 400 and keeps the record" do
|
|
64
|
+
delete daily_report_path(@report)
|
|
65
|
+
assert_refused :bad_request
|
|
66
|
+
assert DailyReport.exists?(@report.id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test "revert without a nested row frame is a 400 and changes nothing" do
|
|
70
|
+
@report.update!(name: "Changed")
|
|
71
|
+
version = @report.versions.where(event: "update").last
|
|
72
|
+
|
|
73
|
+
post revert_daily_report_path(version.id, update: "daily_report_#{@report.id}"),
|
|
74
|
+
headers: stream_headers("daily_report_#{@report.id}")
|
|
75
|
+
assert_refused :bad_request
|
|
76
|
+
assert_equal "Changed", @report.reload.name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ---- the nested forms still work (regression guard) ----------------------
|
|
80
|
+
|
|
81
|
+
test "the nested list, new form, create, row and destroy still render" do
|
|
82
|
+
get daily_reports_path(nested_params), headers: frame_headers(@list_frame)
|
|
83
|
+
assert_response :success
|
|
84
|
+
assert_includes response.body, "Monday"
|
|
85
|
+
|
|
86
|
+
get new_daily_report_path(nested_params), headers: frame_headers(@list_frame)
|
|
87
|
+
assert_response :success
|
|
88
|
+
|
|
89
|
+
assert_difference -> { DailyReport.count }, +1 do
|
|
90
|
+
post daily_reports_path(nested_params), params: { name: "Tuesday" }, headers: frame_headers(@list_frame)
|
|
91
|
+
end
|
|
92
|
+
assert_response :success
|
|
93
|
+
assert_equal @machine.id, DailyReport.find_by!(name: "Tuesday").machine_id
|
|
94
|
+
|
|
95
|
+
get daily_report_path(@report, update: @row_frame), headers: frame_headers(@row_frame)
|
|
96
|
+
assert_response :success
|
|
97
|
+
assert_includes response.body, %(<turbo-frame id="#{@row_frame}">)
|
|
98
|
+
|
|
99
|
+
delete daily_report_path(@report, update: @row_frame), headers: frame_headers(@row_frame)
|
|
100
|
+
assert_response :success
|
|
101
|
+
refute DailyReport.exists?(@report.id)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
test "a nested revert still restores the record" do
|
|
105
|
+
@report.update!(name: "Changed")
|
|
106
|
+
version = @report.versions.where(event: "update").last
|
|
107
|
+
|
|
108
|
+
post revert_daily_report_path(version.id, update: @row_frame), headers: stream_headers(@row_frame)
|
|
109
|
+
assert_response :success
|
|
110
|
+
assert_equal "Monday", @report.reload.name
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ---- accessible model ------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
test "an accessible model's list and row still render" do
|
|
116
|
+
get machines_path
|
|
117
|
+
assert_response :success
|
|
118
|
+
assert_includes response.body, "Press"
|
|
119
|
+
|
|
120
|
+
get machine_path(@machine)
|
|
121
|
+
assert_response :success
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test "an accessible model's new and create without update are a 400; nothing is created" do
|
|
125
|
+
get new_machine_path
|
|
126
|
+
assert_refused :bad_request
|
|
127
|
+
|
|
128
|
+
assert_no_difference -> { Machine.count } do
|
|
129
|
+
post machines_path, params: { name: "Lathe" }
|
|
130
|
+
end
|
|
131
|
+
assert_refused :bad_request
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
test "destroy by a user who may not hard-destroy is a 403 and keeps the record" do
|
|
135
|
+
ApplicationController.dummy_user_superadmin = false
|
|
136
|
+
frame = "machine_#{@machine.id}"
|
|
137
|
+
|
|
138
|
+
delete machine_path(@machine, update: frame), headers: frame_headers(frame)
|
|
139
|
+
assert_refused :forbidden
|
|
140
|
+
assert Machine.exists?(@machine.id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ---- file slots ------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
test "a refused file-slot request still counts as authorized" do
|
|
146
|
+
document = Document.create!(title: "Plan")
|
|
147
|
+
frame = "document_#{document.id}_filename"
|
|
148
|
+
|
|
149
|
+
post remove_file_document_path(document, attribute: "title", update: frame), headers: stream_headers(frame)
|
|
150
|
+
assert_refused :bad_request
|
|
151
|
+
end
|
|
152
|
+
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.56
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ace Suares
|
|
@@ -282,6 +282,7 @@ files:
|
|
|
282
282
|
- test/archived_form_elements_test.rb
|
|
283
283
|
- test/attribute_list_test.rb
|
|
284
284
|
- test/dummy/app/controllers/application_controller.rb
|
|
285
|
+
- test/dummy/app/controllers/daily_reports_controller.rb
|
|
285
286
|
- test/dummy/app/controllers/documents_controller.rb
|
|
286
287
|
- test/dummy/app/controllers/dossiers_controller.rb
|
|
287
288
|
- test/dummy/app/controllers/gizmos_controller.rb
|
|
@@ -292,6 +293,7 @@ files:
|
|
|
292
293
|
- test/dummy/app/helpers/application_helper.rb
|
|
293
294
|
- test/dummy/app/models/ability.rb
|
|
294
295
|
- test/dummy/app/models/application_record.rb
|
|
296
|
+
- test/dummy/app/models/daily_report.rb
|
|
295
297
|
- test/dummy/app/models/document.rb
|
|
296
298
|
- test/dummy/app/models/dossier.rb
|
|
297
299
|
- test/dummy/app/models/gizmo.rb
|
|
@@ -308,11 +310,13 @@ files:
|
|
|
308
310
|
- test/dummy/config/initializers/paper_trail_yaml_safe_load.rb
|
|
309
311
|
- test/dummy/config/routes.rb
|
|
310
312
|
- test/form_element_from_callee_test.rb
|
|
313
|
+
- test/gem_package_modes_test.rb
|
|
311
314
|
- test/inline_edit_polymorphic_path_test.rb
|
|
312
315
|
- test/inline_forms_addto_generator_test.rb
|
|
313
316
|
- test/inline_forms_generator_test.rb
|
|
314
317
|
- test/integration/field_authorization_test.rb
|
|
315
318
|
- test/integration/file_slots_test.rb
|
|
319
|
+
- test/integration/html_refusal_test.rb
|
|
316
320
|
- test/integration/inline_forms_crud_test.rb
|
|
317
321
|
- test/integration/layout_title_test.rb
|
|
318
322
|
- test/integration/legacy_elements_test.rb
|
|
@@ -362,6 +366,7 @@ test_files:
|
|
|
362
366
|
- test/archived_form_elements_test.rb
|
|
363
367
|
- test/attribute_list_test.rb
|
|
364
368
|
- test/dummy/app/controllers/application_controller.rb
|
|
369
|
+
- test/dummy/app/controllers/daily_reports_controller.rb
|
|
365
370
|
- test/dummy/app/controllers/documents_controller.rb
|
|
366
371
|
- test/dummy/app/controllers/dossiers_controller.rb
|
|
367
372
|
- test/dummy/app/controllers/gizmos_controller.rb
|
|
@@ -372,6 +377,7 @@ test_files:
|
|
|
372
377
|
- test/dummy/app/helpers/application_helper.rb
|
|
373
378
|
- test/dummy/app/models/ability.rb
|
|
374
379
|
- test/dummy/app/models/application_record.rb
|
|
380
|
+
- test/dummy/app/models/daily_report.rb
|
|
375
381
|
- test/dummy/app/models/document.rb
|
|
376
382
|
- test/dummy/app/models/dossier.rb
|
|
377
383
|
- test/dummy/app/models/gizmo.rb
|
|
@@ -388,11 +394,13 @@ test_files:
|
|
|
388
394
|
- test/dummy/config/initializers/paper_trail_yaml_safe_load.rb
|
|
389
395
|
- test/dummy/config/routes.rb
|
|
390
396
|
- test/form_element_from_callee_test.rb
|
|
397
|
+
- test/gem_package_modes_test.rb
|
|
391
398
|
- test/inline_edit_polymorphic_path_test.rb
|
|
392
399
|
- test/inline_forms_addto_generator_test.rb
|
|
393
400
|
- test/inline_forms_generator_test.rb
|
|
394
401
|
- test/integration/field_authorization_test.rb
|
|
395
402
|
- test/integration/file_slots_test.rb
|
|
403
|
+
- test/integration/html_refusal_test.rb
|
|
396
404
|
- test/integration/inline_forms_crud_test.rb
|
|
397
405
|
- test/integration/layout_title_test.rb
|
|
398
406
|
- test/integration/legacy_elements_test.rb
|