inline_forms 8.1.56 → 8.1.58
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 +97 -0
- data/app/assets/stylesheets/inline_forms/_theme.scss +13 -0
- data/app/assets/stylesheets/inline_forms/devise.scss +4 -1
- data/app/assets/stylesheets/inline_forms/foundation_and_overrides.scss +1 -0
- data/app/assets/stylesheets/inline_forms/inline_forms.scss +35 -16
- data/app/controllers/inline_forms_controller.rb +50 -6
- 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/dropdown_helper.rb +1 -1
- 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/version.rb +1 -1
- data/lib/locales/inline_forms.en.yml +23 -1
- data/lib/locales/inline_forms.nl.yml +26 -1
- data/test/dummy/config/application.rb +11 -0
- data/test/integration/csp_nonce_test.rb +50 -0
- data/test/integration/dropdown_test.rb +17 -0
- data/test/integration/file_slots_test.rb +91 -0
- data/test/integration/list_params_test.rb +200 -0
- data/test/integration/locale_test.rb +131 -0
- data/test/integration/presentation_escaping_test.rb +80 -0
- metadata +11 -2
- data/app/views/inline_forms/_flash.html.erb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e989cef82d52e8f127898955b5741aa163ac8018c5d376a1031d2b7fc4606b36
|
|
4
|
+
data.tar.gz: 1dd15f90a192376d102aa5247502537a4427388517601dd3b4daba091a4a7385
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aef0b60a06a818a73ebc92defa6d2466f94aede8a39865870b3b3365e508b00642afafe05614addcc3ac0e3b2bf67a0bf3659531ef834c44050014e2bf2e3401
|
|
7
|
+
data.tar.gz: 4b478b2e1854e9c6427e13014f0694883f362184c86b6dfecfa25e9c3ef71752ca60c807c66b2d00f78eaa69dbed73b29afdc1138caece51e818ac340b9e3542
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,103 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [8.1.58] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
Three gem issues found in the StProject review (2026-10-04). The fourth it listed, the CSS `@import` of `http://fonts.googleapis.com`, was already fixed in 8.1.22 (self-hosted Open Sans). Also the first step of the Sass `@import` → `@use` move.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Sass: the entry points load `foundation_and_overrides` with `@use`.** `inline_forms/inline_forms.scss` and `inline_forms/devise.scss` replace `@import 'foundation_and_overrides'` with `@use 'foundation_and_overrides' as *` plus `@forward 'foundation_and_overrides'`. Dart Sass 3 removes `@import`, and these were two of the five gem `@import`s a host build reported.
|
|
14
|
+
- The compiled `inline_forms.css` and `devise.css` are **byte-identical** to 8.1.57's (compressed, compiled with the host's dartsass-rails command; same SHA-256).
|
|
15
|
+
- One rule needed spelling out to stay identical: Foundation's flex grid makes `.columns` an alias with `.columns { @extend .column }`, which reached the gem's `.column { padding-right: 0 !important }` only while the file was `@import`ed. That rule is now `.column, .columns`.
|
|
16
|
+
- The `@forward` keeps the module's API for hosts: Foundation's members (`rem-calc`, `$global-width`, ...) stay visible through `@use "inline_forms/inline_forms" as *`, and the `!default` `$if-*` palette can still be set with `@use "inline_forms/inline_forms" with ($if-accent: ...)`. Without it, both fail to compile.
|
|
17
|
+
- The URL is relative, so it resolves next to the gem's file first, as the `@import` did; a host's `app/assets/stylesheets/inline_forms/foundation_and_overrides.scss` or anything in `app/assets/builds` cannot shadow it.
|
|
18
|
+
- Deprecations per entry point (with `--verbose`): 371 → 370 (`[import]` 118 → 117, `[if-function]` 58, `[global-builtin]` 195). Without `--verbose` Sass prints 5 per category and "356 → 355 repetitive deprecation warnings omitted".
|
|
19
|
+
- **Still `@import`, inside `foundation_and_overrides.scss`:** `settings/settings` and `foundation` (foundation-rails 6.9 is `@import`-only and its settings globals feed the overrides), and `theme`. A `@use 'theme'` would have to precede the settings `@import`, which moves the theme's CSS ahead of Foundation's and runs before `$body-background` exists; `meta.load-css` keeps the position but hides the `$if-*` palette from hosts. All three wait for the Foundation decision. The remaining 114 `[import]`, and all `[if-function]` / `[global-builtin]` warnings, are inside foundation-rails. The deadline is a dartsass-rails release that allows sass-embedded ≥ 3 (0.5.1 pins `~> 1.63`), not a date. No `--quiet-deps` / `--silence-deprecation` was added.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`dropdown` options are sorted again.** `dropdown_edit` called `values.sort_by { _presentation }` and discarded the result, so the options came out in id order. They are now sorted by their (dropdown) presentation.
|
|
24
|
+
- **The failed-create flash header is translated.** `InlineFormsController#create` set `flash[:header]` to the hard-coded Dutch `"Kan <class_name> niet aanmaken."` (with the underscored class name, e.g. `client`) in every locale. It now uses the new key `inline_forms.view.create_failed` with the model's human name: "Could not create %{model}." (en), "Kan %{model} niet aanmaken." (nl). A host with its own locale needs that key.
|
|
25
|
+
- **"+" and close icons on the gold bands meet WCAG contrast.** `.new_button` / `.close_button` in the model top bar and the associated header were the accent colour on gold: 2.9:1 on the model top bar (below the 3:1 for icons), and white on hover (1.8:1). They now use two new variables, `--if-color-on-band` and `--if-color-on-band-hover` (Sass `$if-on-band` / `$if-on-band-hover`), which clear 4.5:1 on both bands in every preset theme (default, dark, sepia, high-contrast). A host that overrides `--if-color-secondary` / `--if-color-header-band` should set them too. The associated header's close-button hover rule also targeted `.new_button:focus` instead of `.close_button:focus`.
|
|
26
|
+
- **Installer: generated apps compile the gem's SCSS on every Dart Sass build.** The `config/initializers/inline_forms_dartsass_builds.rb` template maps `inline_forms_install/{inline_forms,devise}_main.scss` (`@use "inline_forms/inline_forms" as *`) to `app/assets/builds/inline_forms/{inline_forms,devise}.css`. dartsass-rails passes `app/assets/stylesheets` and then every `config.assets.paths` entry as load paths, and `app/assets/builds` comes before the gem, so from the second build on the `@use` resolved to the app's previous compiled `.css` instead of the gem's `.scss`. Gem stylesheet changes stopped reaching an app after its first build (and the example-app gate did not test them). The template now adds `--load-path=<inline_forms>/app/assets/stylesheets` to `config.dartsass.build_options`, which precede the load paths, as StProject's own initializer already does. **Existing apps** generated by an earlier installer should add the same two lines to their initializer.
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
|
|
30
|
+
- `dropdown_test` (options sorted by presentation), `locale_test` (failure header in en and nl, `inline_forms.view.create_failed` in both locale files).
|
|
31
|
+
- Example app: `example_app_sass_deprecations_test` compiles both gem entry points with sass-embedded (verbose) and fails if the gem gains an `@import` beyond the three above, or an entry point has more than 117 `[import]` deprecations.
|
|
32
|
+
- Example app: `example_app_dartsass_build_test` checks that the gem's stylesheet dir is the first load path in the dartsass command, and that `app/assets/builds/inline_forms/{inline_forms,devise}.css` match a direct compile of the gem's entry points (test_helper's `dartsass:build` runs after the installer's, so builds/ already held a previous build).
|
|
33
|
+
|
|
34
|
+
### Lockstep
|
|
35
|
+
|
|
36
|
+
- validation_hints 8.1.58, inline_forms_installer 8.1.58, inline_forms_schema_edit 8.1.58.
|
|
37
|
+
|
|
38
|
+
## [8.1.57] - 2026-10-05
|
|
39
|
+
|
|
40
|
+
Fixes from a security/UI review of a host app (StProject), plus missing and hard-coded translations found when StProject switched to `:nl` with `raise_on_missing_translations = :strict`.
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
|
|
44
|
+
- **Reflected XSS through the nested list's frame id.** `_list` built the list frame as `raw "<turbo-frame id=\"#{update_span}\" …>"`, with `update_span` made from `params[:parent_id]`, and nothing looked the parent up before rendering. So `/incidents?parent_class=Client&ul_needed=1&update=x&parent_id="><img src=x onerror=…>` rendered the payload. Two fixes:
|
|
45
|
+
- The list body is now captured and wrapped with `tag.turbo_frame(id:, class: "list_container")`, so the id is attribute-escaped. No other view or helper puts a request param into `raw` / `html_safe` markup.
|
|
46
|
+
- `InlineFormsController` refuses malformed parent params with **400** in `index`, `new`, `create` and `show` (new `before_action :refuse_malformed_parent_params`). `parent_id` must match `/\A\d+\z/`. `parent_class` must name the class of a non-polymorphic `belongs_to` of `@Klass` that is named after it (`Photo#apartment` for `parent_class=Apartment`), which is exactly what the gem's own links send. The check runs before `_list`'s `constantize` and before `new`/`create` write the foreign key, and after `load_and_authorize_resource`, so a host's `check_authorization` stays satisfied.
|
|
47
|
+
- **Stored XSS in `info_list`.** `info_list_show` appended `item._presentation` unescaped and then marked the string `html_safe`. A `_presentation` built from user data (StProject: `User#name` listed on a Role) therefore ran as markup. It is now escaped with `ERB::Util.h`. Audit of the other helpers that mark strings `html_safe`:
|
|
48
|
+
- `file_field_show` now escapes the stored file name and URL.
|
|
49
|
+
- The versions list rendered a rich-text changeset with `raw`. It is now passed through `sanitize`, so a body posted outside Trix cannot carry script.
|
|
50
|
+
- `dropdown`, `dropdown_with_other`, `check_list`, `radio_button`, the list rows, `_close`, `_show` and the trash pages already escaped `_presentation` (via `link_to` / `link_to_inline_edit` / `h` / ERB) and are unchanged.
|
|
51
|
+
- **Hosts with their own `inline_forms/_versions_list.html.erb` (StProject has one) keep the `raw`** until they apply the same change.
|
|
52
|
+
- **Restoring a trashed file verifies its bytes.** `InlineForms::TrashedFile#restore!` and `InlineForms::StoredFiles.prepare_revert!` (undo of a destroy) now compare the SHA-256 of `data` with the stored `checksum` before writing bytes back into the slot (new `TrashedFile.verify_checksum!`). On a mismatch they log an error (`inline_forms file_trash checksum mismatch entry=<id>`) and raise `NotRestorable`: the transaction rolls back, nothing is written and the row stays in the trash. The field shows "can no longer be restored". The revert answers **422** instead of a 500; this also covers a `NotRestorable` from a concurrent purge.
|
|
53
|
+
- **Inline scripts carry the CSP nonce.** The Turbo module `<script>` in `layouts/inline_forms` and `layouts/application` is now rendered with `nonce: content_security_policy_nonce`, which adds no attribute without a nonce generator. It is also marked `data-turbo-eval="false"`, and both layouts emit `csp_meta_tag`. This is the same as StProject's own layout, so a `script-src 'nonce-…'` policy accepts the gem's layouts.
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- **`inline_forms/_flash.html.erb`.** It had an inline jQuery `<script>` (no nonce), and neither the gem, the installer, the example app nor StProject rendered it. The layouts show flash themselves, and Devise pages use `devise/sessions/_flash`. A host that still does `render "inline_forms/flash"` must provide its own partial.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **File-slot actions keep the page's slots.** `FileSlotsConcern` has no per-tab `@inline_forms_attribute_list`. After a remove, restore or undo, the re-rendered field (`_file_slot`) and the versions panel built `slots=` from the model's full attribute list. On a tabbed host (StProject's Client), the next action's trash panel then listed files from other tabs' slots. `inline_forms_file_trash_slots` now uses the action's `@file_trash_slots` for its record. That value comes from the page's own `slots` param and is reduced to declared, listed, readable slots, as before.
|
|
62
|
+
- **Pagination of the top-level list keeps a host's filters, now under test.** A host's own filters (`first_name`, `sorteer`, `group[id]`, `status[id]`, …) stay on page 2. will_paginate already did that: it puts the request's params into every page link (all params on GET, the query string otherwise). The only change is that the redundant explicit `search` param is dropped. The page links deliberately do **not** merge `request.query_parameters` themselves. That skips will_paginate's exclusion of `script_name` / `original_script_name`, so `?script_name=javascript:alert(1)//` would become the start of every page link's `href` (XSS), and `?controller=x` would raise a 500.
|
|
63
|
+
|
|
64
|
+
- **The create flash was never translated.** `InlineFormsController#create` calls `t("success", message: …)`, but both locale files defined the key as `succes`, so every create looked up a missing translation (and raised under `raise_on_missing_translations`). The key is now `success` (en "%{message} created.", nl "%{message} is aangemaakt."). The misspelled `succes` is dropped: it never received the message, and nothing else used it.
|
|
65
|
+
- **The versions panel is translated.** `_versions` and `_versions_list` hard-coded English: "Versions (n)", Event, When, Done by, Changeset, old value / new value, empty, "(rich text)" and the raw event names. They now use `inline_forms.view.versions` (with `count`), `versions_event`, `versions_when`, `versions_done_by`, `versions_changeset`, `versions_old_value`, `versions_new_value`, `versions_empty`, `versions_rich_text` and `version_events.{create,update,destroy}`. Unknown events fall back to the raw name. en and nl are included, with the same key names as StProject's overrides, so those overrides can be dropped.
|
|
66
|
+
- **Missing view keys:**
|
|
67
|
+
- nl now has `inline_forms.view.restore`, `list_versions`, `close_versions_list` and `undelete`.
|
|
68
|
+
- en now has `undelete`, used by the soft-restore link (`link_to_soft_delete`).
|
|
69
|
+
- Both locales now have `common.more` and `common.logout`, which the engine header uses and which only hosts defined before.
|
|
70
|
+
- **Version timestamps are localized.** The versions list and its file events printed `created_at` / the event time raw (`2026-10-05 15:10:24 UTC`). They now use `l(…, format: :short)`.
|
|
71
|
+
- **`<html lang>`.** `layouts/devise` and `layouts/application` now declare `lang="<%= I18n.locale %>"`, like the `inline_forms` layout.
|
|
72
|
+
- **The sign-in button is translated.** `devise/sessions/_form` used `f.submit :login` (always "login"). It now uses `t("inline_forms.devise.login", default: "login")`: en "login", nl "inloggen".
|
|
73
|
+
|
|
74
|
+
### Tests
|
|
75
|
+
|
|
76
|
+
- `test/integration/list_params_test.rb`:
|
|
77
|
+
- a crafted `parent_id` (frame request and full page) → 400 with the payload never in the body;
|
|
78
|
+
- a non-association or wrongly cased `parent_class` → 400, and a non-integer `parent_id` → 400;
|
|
79
|
+
- crafted `new` / `create` / `show` → 400, and `create` creates nothing;
|
|
80
|
+
- every refusal counts as authorized;
|
|
81
|
+
- well-formed nested list, new and create still work, including the `not_accessible_through_html?` child;
|
|
82
|
+
- `_list` escapes the frame id by itself;
|
|
83
|
+
- page 2 of a filtered list (and back to page 1) carries every filter, and so does the list rendered after a create;
|
|
84
|
+
- a crafted `script_name` / `original_script_name` / `controller` in the query string never reaches the page links.
|
|
85
|
+
- `test/integration/presentation_escaping_test.rb`: `info_list`, `file_field`, `dropdown_with_other` and the list rows escape `<img src=x onerror=…>`; the versions list sanitizes a rich-text changeset.
|
|
86
|
+
- `test/integration/file_slots_test.rb`:
|
|
87
|
+
- a restore re-renders with the page's `slots` only, so neither panel lists the other slot's file, and `slots` is reduced to declared slots;
|
|
88
|
+
- a tampered trash row (bytes or checksum) is refused by `restore_file` and by `restore!` (logged, nothing written);
|
|
89
|
+
- undo of a destroy with a tampered row → 422 with nothing restored.
|
|
90
|
+
- `test/integration/csp_nonce_test.rb`: the dummy app now runs a nonce CSP (report-only, like StProject). Both layouts' inline scripts carry the request's nonce and publish it in `csp-nonce`, and `inline_forms/_flash` is gone.
|
|
91
|
+
- `test/integration/locale_test.rb`. The dummy app now loads `rails-i18n`, as host Gemfiles do. Tests:
|
|
92
|
+
- every key above exists in en and nl, and `succes` is gone;
|
|
93
|
+
- create's flash is translated in nl;
|
|
94
|
+
- the versions panel and list render in nl with translated labels, event names and a localized timestamp, and in en with the English labels. Both render with missing translations raising, in views and controllers;
|
|
95
|
+
- the devise and application layouts declare `lang`.
|
|
96
|
+
- Example app:
|
|
97
|
+
- `example_app_turbo_layout_test.rb` accepts the Turbo module script with its new attributes;
|
|
98
|
+
- new `example_app_devise_sign_in_test.rb`: the sign-in page has `<html lang>` and the translated submit button.
|
|
99
|
+
|
|
100
|
+
### Lockstep
|
|
101
|
+
|
|
102
|
+
- validation_hints 8.1.57 (now ships an nl locale, and `message: :required` hints, i.e. every `belongs_to`, resolve in en and nl), inline_forms_installer 8.1.57, inline_forms_schema_edit 8.1.57.
|
|
103
|
+
|
|
7
104
|
## [8.1.56] - 2026-10-05
|
|
8
105
|
|
|
9
106
|
### Fixed
|
|
@@ -33,6 +33,11 @@ $if-flash-accent: #fade7a !default; // flash border band
|
|
|
33
33
|
$if-error-bg: #a70f0f !default;
|
|
34
34
|
$if-success-bg: #4f8d0d !default;
|
|
35
35
|
$if-on-dark: #FFFFFF !default; // text/icons on primary/secondary bands
|
|
36
|
+
// The "+" / close icons on the gold bands (secondary, header-band). The
|
|
37
|
+
// accent was 2.9:1 on the secondary gold and the white hover 1.8:1; these
|
|
38
|
+
// clear 4.5:1 on both bands.
|
|
39
|
+
$if-on-band: #8F2F18 !default;
|
|
40
|
+
$if-on-band-hover: #4A1A0C !default;
|
|
36
41
|
|
|
37
42
|
:root {
|
|
38
43
|
--if-color-primary: #{$if-primary};
|
|
@@ -48,6 +53,8 @@ $if-on-dark: #FFFFFF !default; // text/icons on primary/secondary bands
|
|
|
48
53
|
--if-color-error-bg: #{$if-error-bg};
|
|
49
54
|
--if-color-success-bg: #{$if-success-bg};
|
|
50
55
|
--if-color-on-dark: #{$if-on-dark};
|
|
56
|
+
--if-color-on-band: #{$if-on-band};
|
|
57
|
+
--if-color-on-band-hover: #{$if-on-band-hover};
|
|
51
58
|
// Foundation globals, bridged so body/anchors can be re-themed too.
|
|
52
59
|
--if-color-body-bg: #{$body-background};
|
|
53
60
|
--if-color-body-text: #{$body-font-color};
|
|
@@ -85,6 +92,8 @@ body.theme-dark {
|
|
|
85
92
|
--if-color-input-bg-hover: #332e25;
|
|
86
93
|
--if-color-input-bg-focus: #3a352b;
|
|
87
94
|
--if-color-flash-accent: #7a6829;
|
|
95
|
+
--if-color-on-band: #fdeee5;
|
|
96
|
+
--if-color-on-band-hover: #ffffff;
|
|
88
97
|
--if-color-body-bg: #1c1a16;
|
|
89
98
|
--if-color-body-text: #e8e4da;
|
|
90
99
|
--if-color-anchor: #e0a179;
|
|
@@ -108,6 +117,8 @@ body.theme-sepia {
|
|
|
108
117
|
--if-color-input-bg-hover: #fffaf0;
|
|
109
118
|
--if-color-input-bg-focus: #fffdf7;
|
|
110
119
|
--if-color-flash-accent: #dcc078;
|
|
120
|
+
--if-color-on-band: #6e3a20;
|
|
121
|
+
--if-color-on-band-hover: #3a2418;
|
|
111
122
|
--if-color-body-bg: #f4ecd8;
|
|
112
123
|
--if-color-body-text: #5b4636;
|
|
113
124
|
--if-color-anchor: #8b5e3c;
|
|
@@ -124,6 +135,8 @@ body.theme-high-contrast {
|
|
|
124
135
|
--if-color-input-bg-hover: #f5f5f5;
|
|
125
136
|
--if-color-input-bg-focus: #ffffff;
|
|
126
137
|
--if-color-flash-accent: #ffd500;
|
|
138
|
+
--if-color-on-band: #b30000;
|
|
139
|
+
--if-color-on-band-hover: #000000;
|
|
127
140
|
--if-color-body-bg: #ffffff;
|
|
128
141
|
--if-color-body-text: #000000;
|
|
129
142
|
--if-color-anchor: #0000d6;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Open Sans @font-face lives in inline_forms_fonts.css.erb (self-hosted,
|
|
2
2
|
// Sprockets-served; linked from the layouts) — no external font CDN.
|
|
3
|
-
@
|
|
3
|
+
// @use / @forward of foundation_and_overrides: see the module-system note
|
|
4
|
+
// in inline_forms.scss.
|
|
5
|
+
@forward 'foundation_and_overrides';
|
|
6
|
+
@use 'foundation_and_overrides' as *;
|
|
4
7
|
|
|
5
8
|
// Devise pages have only `.contain-to-grid` as the top-bar wrapper (no
|
|
6
9
|
// `.fixed` variant), so constrain it directly to the F5 1000px row
|
|
@@ -55,6 +55,7 @@ $body-font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Roboto, Arial, sans
|
|
|
55
55
|
|
|
56
56
|
// Theme bridge: palette as CSS custom properties (must come after
|
|
57
57
|
// 'foundation' so it can read $body-background / $anchor-color).
|
|
58
|
+
// Stays @import: see the module-system note in inline_forms.scss.
|
|
58
59
|
@import 'theme';
|
|
59
60
|
|
|
60
61
|
@import '../../../vendor/assets/stylesheets/tippy.css';
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
// Open Sans @font-face lives in inline_forms_fonts.css.erb (self-hosted,
|
|
3
3
|
// Sprockets-served; linked from the layouts) — no external font CDN.
|
|
4
4
|
//
|
|
5
|
-
// Module-system status (8.1.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// Module-system status (8.1.58). Loaded with @use:
|
|
6
|
+
// - `foundation_icons` (8.1.24): a self-contained partial.
|
|
7
|
+
// - `foundation_and_overrides`, here and in devise.scss (8.1.58). The
|
|
8
|
+
// compiled CSS is byte-identical to the @import version (the `.columns`
|
|
9
|
+
// alias below is the one rule that needed spelling out). The relative URL
|
|
10
|
+
// resolves next to this file first, as the @import did, so a same-named
|
|
11
|
+
// host or app/assets/builds file cannot shadow it. The @forward keeps what
|
|
12
|
+
// hosts saw through `@use "inline_forms/inline_forms"` before: Foundation's
|
|
13
|
+
// members (rem-calc, $global-width, ...) and the !default `$if-*` palette,
|
|
14
|
+
// configurable with `with (...)`.
|
|
15
|
+
// Still @import, inside foundation_and_overrides:
|
|
16
|
+
// - 'settings/settings' and 'foundation': foundation-rails 6.9 is
|
|
17
|
+
// @import-only and its settings globals feed our overrides. Blocked by the
|
|
18
|
+
// Foundation decision (unmaintained upstream; no module API coming).
|
|
19
|
+
// - 'theme': a @use must precede the settings @import, which would move the
|
|
20
|
+
// theme's CSS ahead of Foundation's and run before $body-background exists;
|
|
21
|
+
// meta.load-css keeps the position but hides the `$if-*` palette from hosts
|
|
22
|
+
// (`with ($if-accent: ...)` stops working). Moves with Foundation.
|
|
23
|
+
// The deadline is a dartsass-rails release that allows sass-embedded >= 3
|
|
24
|
+
// (Dart Sass 3 removes @import; 0.5.1 pins ~> 1.63), not a date.
|
|
25
|
+
// See stuff/improvement-plan.md §1.
|
|
12
26
|
//
|
|
13
27
|
// jQuery UI SCSS (sunny theme + widget styles) was dropped in 8.1.26 along
|
|
14
28
|
// with the last jQuery UI widgets (native date/time/month/range/datalist
|
|
15
29
|
// inputs replaced them).
|
|
16
30
|
@use 'inline_forms/foundation_icons';
|
|
17
|
-
@
|
|
31
|
+
@forward 'foundation_and_overrides';
|
|
32
|
+
@use 'foundation_and_overrides' as *;
|
|
18
33
|
|
|
19
34
|
// Foundation 6 makes `.row` a flex container (`display: flex`); a nested
|
|
20
35
|
// `.row` is therefore a flex item and shrinks to its content's intrinsic
|
|
@@ -223,12 +238,12 @@ select:hover, select:focus {
|
|
|
223
238
|
}
|
|
224
239
|
.new_button {
|
|
225
240
|
background-color: var(--if-color-secondary);
|
|
226
|
-
color: var(--if-color-
|
|
241
|
+
color: var(--if-color-on-band);
|
|
227
242
|
font-size: 1.3rem;
|
|
228
243
|
padding: 0 2rem 0 1rem;
|
|
229
244
|
}
|
|
230
245
|
.new_button:hover, .new_button:focus {
|
|
231
|
-
color:
|
|
246
|
+
color: var(--if-color-on-band-hover);
|
|
232
247
|
-webkit-transition-property: color;
|
|
233
248
|
transition-property: color;
|
|
234
249
|
}
|
|
@@ -358,26 +373,26 @@ turbo-frame {
|
|
|
358
373
|
.new_button {
|
|
359
374
|
background-color: var(--if-color-header-band);
|
|
360
375
|
font-size: 1.5rem;
|
|
361
|
-
color: var(--if-color-
|
|
376
|
+
color: var(--if-color-on-band);
|
|
362
377
|
padding: 0 0.5rem 0 1rem;
|
|
363
378
|
margin: 0;
|
|
364
379
|
}
|
|
365
380
|
.new_button:hover, .new_button:focus {
|
|
366
381
|
background-color: var(--if-color-header-band);
|
|
367
|
-
color:
|
|
382
|
+
color: var(--if-color-on-band-hover);
|
|
368
383
|
-webkit-transition-property: color;
|
|
369
384
|
transition-property: color;
|
|
370
385
|
}
|
|
371
386
|
.close_button {
|
|
372
387
|
background-color: var(--if-color-header-band);
|
|
373
388
|
font-size: 1.5rem;
|
|
374
|
-
color: var(--if-color-
|
|
389
|
+
color: var(--if-color-on-band);
|
|
375
390
|
padding: 0 0.5rem 0 1rem;
|
|
376
391
|
margin: 0;
|
|
377
392
|
}
|
|
378
|
-
.close_button:hover, .
|
|
393
|
+
.close_button:hover, .close_button:focus {
|
|
379
394
|
background-color: var(--if-color-header-band);
|
|
380
|
-
color:
|
|
395
|
+
color: var(--if-color-on-band-hover);
|
|
381
396
|
-webkit-transition-property: color;
|
|
382
397
|
transition-property: color;
|
|
383
398
|
}
|
|
@@ -423,7 +438,11 @@ turbo-frame {
|
|
|
423
438
|
margin-bottom: 1.5em;
|
|
424
439
|
}
|
|
425
440
|
|
|
426
|
-
|
|
441
|
+
// `.columns` named explicitly: Foundation's flex grid aliases it with
|
|
442
|
+
// `.columns { @extend .column }`, which reached this rule while
|
|
443
|
+
// foundation_and_overrides was @imported. Since it is @used (8.1.58),
|
|
444
|
+
// that @extend no longer crosses into this module.
|
|
445
|
+
.column, .columns {
|
|
427
446
|
padding-right: 0 !important;
|
|
428
447
|
}
|
|
429
448
|
|
|
@@ -32,6 +32,9 @@ class InlineFormsController < ApplicationController
|
|
|
32
32
|
|
|
33
33
|
# +revert+ and the file-slot actions authorize themselves (attribute-level).
|
|
34
34
|
load_and_authorize_resource except: [ :revert, *FileSlotsConcern::FILE_SLOT_ACTIONS ], no_params: true if cancan_enabled?
|
|
35
|
+
# After the authorization above, so a refusal still satisfies a host's
|
|
36
|
+
# check_authorization (see refuse_malformed_parent_params).
|
|
37
|
+
before_action :refuse_malformed_parent_params, only: [ :index, :new, :create, :show ]
|
|
35
38
|
# :index shows a list of all objects from class @Klass, using will_paginate,
|
|
36
39
|
# including a link to 'new', that allows you to create a new record.
|
|
37
40
|
def index
|
|
@@ -186,7 +189,7 @@ class InlineFormsController < ApplicationController
|
|
|
186
189
|
format.html { render_list_frame_after_save }
|
|
187
190
|
end
|
|
188
191
|
else
|
|
189
|
-
flash.now[:header] = [ "
|
|
192
|
+
flash.now[:header] = [ t("inline_forms.view.create_failed", model: @object.class.model_name.human) ]
|
|
190
193
|
flash.now[:error] = @object.errors.to_a
|
|
191
194
|
respond_to do |format|
|
|
192
195
|
@object.inline_forms_attribute_list = attributes
|
|
@@ -380,11 +383,18 @@ class InlineFormsController < ApplicationController
|
|
|
380
383
|
else
|
|
381
384
|
# One transaction: the record, its files coming back out of the trash
|
|
382
385
|
# (undo of a destroy) and its rich texts are restored together or not.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
begin
|
|
387
|
+
@object.class.transaction do
|
|
388
|
+
file_entries = InlineForms::StoredFiles.prepare_revert!(@object)
|
|
389
|
+
@parent = persist_reverted_primary!(@object, @version)
|
|
390
|
+
InlineForms::StoredFiles.finish_revert!(file_entries, by: inline_forms_current_user_id)
|
|
391
|
+
restore_rich_texts_for_reverted_parent!(@parent)
|
|
392
|
+
end
|
|
393
|
+
rescue InlineForms::TrashedFile::NotRestorable
|
|
394
|
+
# A trashed file failed its checksum, or was purged/restored
|
|
395
|
+
# concurrently: the transaction rolled back and nothing was restored.
|
|
396
|
+
# An explicit answer instead of a 500.
|
|
397
|
+
return head :unprocessable_entity
|
|
388
398
|
end
|
|
389
399
|
@parent.reload
|
|
390
400
|
end
|
|
@@ -393,6 +403,40 @@ class InlineFormsController < ApplicationController
|
|
|
393
403
|
|
|
394
404
|
private
|
|
395
405
|
|
|
406
|
+
# +parent_class+ / +parent_id+ come from the nested-list links and are
|
|
407
|
+
# client-controlled, yet +_list+ constantizes +parent_class+, +new+ and
|
|
408
|
+
# +create+ write +parent_id+ into the foreign key, and both land in the
|
|
409
|
+
# list's frame ids. Nothing +find+s the parent before the list renders, so a
|
|
410
|
+
# crafted +parent_id+ (markup) reached the page (reflected XSS), and any
|
|
411
|
+
# +parent_class+ string reached +constantize+. Refuse (400) unless
|
|
412
|
+
# +parent_id+ is a plain integer id and +parent_class+ names the class of a
|
|
413
|
+
# belongs_to association of @Klass named after it (Photo#apartment for
|
|
414
|
+
# +parent_class=Apartment+): exactly what the gem's own links send.
|
|
415
|
+
def refuse_malformed_parent_params
|
|
416
|
+
parent_class = params[:parent_class]
|
|
417
|
+
parent_id = params[:parent_id]
|
|
418
|
+
return if parent_class.blank? && parent_id.blank?
|
|
419
|
+
return if parent_id_param?(parent_id) && (parent_class.blank? || parent_class_param?(parent_class))
|
|
420
|
+
|
|
421
|
+
head :bad_request
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def parent_id_param?(parent_id)
|
|
425
|
+
parent_id.is_a?(String) && parent_id.match?(/\A\d+\z/)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Compared against the reflections (no +to_sym+ / +constantize+ of the
|
|
429
|
+
# param): a non-polymorphic belongs_to whose name is the underscored class.
|
|
430
|
+
def parent_class_param?(parent_class)
|
|
431
|
+
return false unless parent_class.is_a?(String)
|
|
432
|
+
|
|
433
|
+
@Klass.reflect_on_all_associations(:belongs_to).any? do |reflection|
|
|
434
|
+
!reflection.polymorphic? &&
|
|
435
|
+
reflection.class_name == parent_class &&
|
|
436
|
+
reflection.name.to_s == parent_class.underscore
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
396
440
|
# Single-field requests (edit, update, show with an attribute) name the
|
|
397
441
|
# field and its form element in params, and both are client-controlled.
|
|
398
442
|
# +load_and_authorize_resource+ only authorizes the record: CanCanCan's
|
|
@@ -24,7 +24,17 @@ module InlineFormsFileTrashHelper
|
|
|
24
24
|
|
|
25
25
|
# Declared, routed slots among the rows of the attribute list being shown
|
|
26
26
|
# (a Client tab shows only its own slots).
|
|
27
|
+
#
|
|
28
|
+
# The file-slot actions (FileSlotsConcern) re-render the field, the trash
|
|
29
|
+
# panel and the versions panel as streams, and that controller has no
|
|
30
|
+
# per-tab @inline_forms_attribute_list: falling back to the model's full
|
|
31
|
+
# list put every tab's slots into the re-rendered links (slots=…), so the
|
|
32
|
+
# next action's panel listed other tabs' files. Those actions set
|
|
33
|
+
# @file_trash_slots from the page's own +slots+ param (validated against
|
|
34
|
+
# the declared, listed, readable slots); use it for their record.
|
|
27
35
|
def inline_forms_file_trash_slots(object, attributes = nil)
|
|
36
|
+
return Array(@file_trash_slots) if attributes.nil? && !@file_trash_slots.nil? && object == @object
|
|
37
|
+
|
|
28
38
|
attributes ||= @inline_forms_attribute_list || object.inline_forms_attribute_list
|
|
29
39
|
attributes.filter_map do |attribute, form_element, *|
|
|
30
40
|
next unless form_element.to_s == "simple_file_field"
|
|
@@ -56,7 +56,8 @@ module InlineForms::StoredFiles
|
|
|
56
56
|
# * undo of a destroy (row gone): copy each slot's newest restorable
|
|
57
57
|
# record_destroyed entry onto the record BEFORE the save, so presence
|
|
58
58
|
# validations on the filename pass; a slot whose file was purged in the
|
|
59
|
-
# meantime comes back empty (never a name without bytes).
|
|
59
|
+
# meantime comes back empty (never a name without bytes). An entry whose
|
|
60
|
+
# bytes fail their SHA-256 check raises NotRestorable (nothing restored).
|
|
60
61
|
# * row exists already (replayed undo): nothing; persist_reverted_primary!
|
|
61
62
|
# leaves the slot columns alone.
|
|
62
63
|
def self.prepare_revert!(record)
|
|
@@ -73,6 +74,9 @@ module InlineForms::StoredFiles
|
|
|
73
74
|
entry = InlineForms::TrashedFile.lock.for_slot(record, attribute)
|
|
74
75
|
.where(reason: "record_destroyed").restorable.newest_first.first
|
|
75
76
|
if entry
|
|
77
|
+
# Same integrity check as TrashedFile#restore!: a mismatch raises
|
|
78
|
+
# NotRestorable and the whole revert rolls back.
|
|
79
|
+
InlineForms::TrashedFile.verify_checksum!(id: entry.id, data: entry.data, checksum: entry.checksum)
|
|
76
80
|
record.send(:write_attribute, slot[:data], entry.data)
|
|
77
81
|
record.send(:write_attribute, attribute.to_s, entry.filename)
|
|
78
82
|
record.send(:write_attribute, slot[:content_type], entry.content_type)
|
|
@@ -85,6 +85,18 @@ class InlineForms::TrashedFile < ActiveRecord::Base
|
|
|
85
85
|
)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Restoring writes the stored bytes back into a live slot, so they must be
|
|
89
|
+
# the bytes that were trashed: compares the SHA-256 recorded by trash!
|
|
90
|
+
# (nil data has a nil checksum). Raises NotRestorable on a mismatch, before
|
|
91
|
+
# anything is written, so the caller's transaction rolls back; the mismatch
|
|
92
|
+
# is logged (a tampered or corrupted trash row needs a look).
|
|
93
|
+
def self.verify_checksum!(id:, data:, checksum:)
|
|
94
|
+
return if checksum == (data && Digest::SHA256.hexdigest(data))
|
|
95
|
+
|
|
96
|
+
Rails.logger.error("inline_forms file_trash checksum mismatch entry=#{id}: not restored")
|
|
97
|
+
raise NotRestorable, "this file failed its integrity check and cannot be restored"
|
|
98
|
+
end
|
|
99
|
+
|
|
88
100
|
# Purges every trash row whose retention has passed. Returns the number
|
|
89
101
|
# purged. Idempotent; safe to run while users work.
|
|
90
102
|
def self.purge_expired!
|
|
@@ -189,14 +201,16 @@ class InlineForms::TrashedFile < ActiveRecord::Base
|
|
|
189
201
|
record = record_class.unscoped.lock.find_by(record_class.primary_key => record_id)
|
|
190
202
|
raise RecordGone, "the record was deleted" unless record
|
|
191
203
|
|
|
192
|
-
payload = self.class.lock.where(id: id).restorable.pick(:data, :filename, :content_type)
|
|
204
|
+
payload = self.class.lock.where(id: id).restorable.pick(:data, :filename, :content_type, :checksum)
|
|
193
205
|
raise NotRestorable, "this file can no longer be restored" unless payload
|
|
194
206
|
|
|
207
|
+
data, filename, content_type, checksum = payload
|
|
208
|
+
self.class.verify_checksum!(id: id, data: data, checksum: checksum)
|
|
209
|
+
|
|
195
210
|
now = Time.current
|
|
196
211
|
count = self.class.where(id: id).restorable.update_all(data: nil, restored_at: now, restored_by_id: by, updated_at: now)
|
|
197
212
|
raise NotRestorable, "this file can no longer be restored" unless count == 1
|
|
198
213
|
|
|
199
|
-
data, filename, content_type = payload
|
|
200
214
|
record.inline_forms_put_file!(attribute_name, data: data, filename: filename, content_type: content_type, by: by)
|
|
201
215
|
reload
|
|
202
216
|
record
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
<%= f.check_box :remember_me %><label><%= t :remember_me %></label>
|
|
27
27
|
</div>
|
|
28
28
|
<div class="small-7 columns text-right">
|
|
29
|
-
<%= f.submit :login, class: 'button' %>
|
|
29
|
+
<%= f.submit t("inline_forms.devise.login", default: "login"), class: 'button' %>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
<% else %>
|
|
33
33
|
<div class="row">
|
|
34
34
|
<div class="columns text-right">
|
|
35
|
-
<%= f.submit :login, class: 'button' %>
|
|
35
|
+
<%= f.submit t("inline_forms.devise.login", default: "login"), class: 'button' %>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
<% end -%>
|
|
@@ -42,11 +42,16 @@
|
|
|
42
42
|
<% end %>
|
|
43
43
|
<% end %>
|
|
44
44
|
|
|
45
|
-
<%#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
<%#
|
|
46
|
+
List root is `<turbo-frame class="list_container">` (Step 4). See SCSS.
|
|
47
|
+
The rows and pagination are captured and wrapped with tag.turbo_frame at
|
|
48
|
+
the end, so the frame id is attribute-escaped: it is built from request
|
|
49
|
+
params (parent_class / parent_id / update), and the former
|
|
50
|
+
`raw "<turbo-frame id=\"#{update_span}\" …>"` put a crafted parent_id
|
|
51
|
+
straight into the page (reflected XSS). The controller also refuses
|
|
52
|
+
malformed parent params (refuse_malformed_parent_params).
|
|
53
|
+
-%>
|
|
54
|
+
<% list_body = capture do -%>
|
|
50
55
|
<!-- # list of objects -->
|
|
51
56
|
<% for object in objects %>
|
|
52
57
|
<% if parent_class.nil? %>
|
|
@@ -111,8 +116,17 @@
|
|
|
111
116
|
<% end -%>
|
|
112
117
|
<!-- # pagination -->
|
|
113
118
|
<% if parent_id.nil? -%>
|
|
119
|
+
<%#
|
|
120
|
+
Page 2 of a filtered list must stay filtered (search and a host's own
|
|
121
|
+
filters such as first_name / sorteer / group[id]). will_paginate does
|
|
122
|
+
that itself: it puts the request's params (all params on GET, the query
|
|
123
|
+
string otherwise) into every page link, except script_name and
|
|
124
|
+
original_script_name. Do not merge request.query_parameters in here:
|
|
125
|
+
explicit params skip that exclusion, so ?script_name=javascript:… became
|
|
126
|
+
the start of every page link's href (XSS), and ?controller=x a 500.
|
|
127
|
+
test/integration/list_params_test.rb covers both.
|
|
128
|
+
-%>
|
|
114
129
|
<% paginate_params = { update: update_span, ul_needed: true } %>
|
|
115
|
-
<% paginate_params[:search] = params[:search] if params[:search].present? %>
|
|
116
130
|
<% pagination = will_paginate objects, params: paginate_params %>
|
|
117
131
|
<% else %>
|
|
118
132
|
<% if pagination %>
|
|
@@ -144,6 +158,9 @@
|
|
|
144
158
|
</div>
|
|
145
159
|
</div>
|
|
146
160
|
<% end %>
|
|
161
|
+
<% end -%>
|
|
147
162
|
<% if ul_needed -%>
|
|
148
|
-
<%=
|
|
163
|
+
<%= tag.turbo_frame(list_body, id: update_span, class: "list_container") -%>
|
|
164
|
+
<% else -%>
|
|
165
|
+
<%= list_body -%>
|
|
149
166
|
<% end -%>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
(parent + rich_text), flickering as the user toggled the panel. -%>
|
|
8
8
|
<div class="row form_element_header associated_auto_header callout">
|
|
9
9
|
<div class='medium-11 large-11 column' >
|
|
10
|
-
<%= "
|
|
10
|
+
<%= t("inline_forms.view.versions", count: inline_forms_history_for(object).length) %>
|
|
11
11
|
</div>
|
|
12
12
|
<div class='medium-1 large-1 column'>
|
|
13
13
|
<%= link_to_versions_list(
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<% restorable_files = inline_forms_restorable_file_entries(@object, entries.filter_map { |e| e[:entry]&.attribute_name }.uniq) -%>
|
|
5
5
|
<div class="row form_element_header associated_auto_header callout">
|
|
6
6
|
<div class='medium-11 large-11 column' >
|
|
7
|
-
<%= "
|
|
7
|
+
<%= t("inline_forms.view.versions", count: entries.length) %>
|
|
8
8
|
</div>
|
|
9
9
|
<div class='medium-1 large-1 column'>
|
|
10
10
|
<%= close_versions_list_link(
|
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
|
|
33
33
|
</div>
|
|
34
34
|
<div class="small-1 column">
|
|
35
|
-
<strong
|
|
35
|
+
<strong><%= t("inline_forms.view.versions_event") %></strong>
|
|
36
36
|
</div>
|
|
37
37
|
<div class="small-2 column">
|
|
38
|
-
<strong
|
|
38
|
+
<strong><%= t("inline_forms.view.versions_when") %></strong>
|
|
39
39
|
</div>
|
|
40
40
|
<div class="small-2 column">
|
|
41
|
-
<strong
|
|
41
|
+
<strong><%= t("inline_forms.view.versions_done_by") %></strong>
|
|
42
42
|
</div>
|
|
43
43
|
<div class="small-6 column">
|
|
44
|
-
<strong
|
|
44
|
+
<strong><%= t("inline_forms.view.versions_changeset") %></strong>
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
47
|
<% entries.reverse.each do |entry| %>
|
|
@@ -92,17 +92,17 @@
|
|
|
92
92
|
<% end %>
|
|
93
93
|
</div>
|
|
94
94
|
<div class="small-1 column">
|
|
95
|
-
<%= version.event -%><% if entry[:kind] == :rich_text %> <em>(
|
|
95
|
+
<%= t("inline_forms.view.version_events.#{version.event}", default: version.event) -%><% if entry[:kind] == :rich_text %> <em>(<%= t("inline_forms.view.versions_rich_text") %>)</em><% end %>
|
|
96
96
|
</div>
|
|
97
97
|
<div class="small-2 column">
|
|
98
|
-
<%= version.created_at -%>
|
|
98
|
+
<%= l(version.created_at, format: :short) -%>
|
|
99
99
|
</div>
|
|
100
100
|
<div class="small-2 column">
|
|
101
101
|
<%= version_modified_by version.whodunnit -%>
|
|
102
102
|
</div>
|
|
103
103
|
<div class="small-6 column">
|
|
104
104
|
<% if version.changeset.nil? || version.changeset.empty? %>
|
|
105
|
-
<em
|
|
105
|
+
<em><%= t("inline_forms.view.versions_empty") %></em>
|
|
106
106
|
<% else %>
|
|
107
107
|
<% version.changeset.each do |attribute, value| %>
|
|
108
108
|
<% next if attribute == 'updated_at' %>
|
|
@@ -116,13 +116,15 @@
|
|
|
116
116
|
<th></th>
|
|
117
117
|
</tr>
|
|
118
118
|
<tr>
|
|
119
|
-
<td
|
|
120
|
-
<td
|
|
119
|
+
<td><%= t("inline_forms.view.versions_old_value") %></td>
|
|
120
|
+
<td><%= t("inline_forms.view.versions_new_value") %></td>
|
|
121
121
|
</tr>
|
|
122
122
|
<tr>
|
|
123
123
|
<% if entry[:kind] == :rich_text && attribute == 'body' %>
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
<%# Rich-text HTML as stored in the version: sanitized, never
|
|
125
|
+
raw (a body posted outside Trix may carry script). -%>
|
|
126
|
+
<td><%= sanitize value[0].to_s %></td>
|
|
127
|
+
<td><%= sanitize value[1].to_s %></td>
|
|
126
128
|
<% else %>
|
|
127
129
|
<td><%= value[0] %></td>
|
|
128
130
|
<td><%= value[1] %></td>
|