diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f04c24..eeb1f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ -### v.2.5.0 +## v.2.5.1 + +### Security + +- Update JS dependencies with high CVE + +### Fixes and improvements + +- Fix form submissions losing values when field names contain spaces or non-word characters + +## v.2.5.0 ### Features @@ -27,7 +37,7 @@ - Fix manager and presenter views while presentation conversion has no slide count yet - Fix crash on event manager pages when an event has multiple activity leaders -### v.2.4.0 +## v.2.4.0 ### ⚠️ Breaking changes @@ -59,7 +69,7 @@ - Fix italian translation (#179) - Fix random poll choices (#184) -### v.2.3.2 +## v.2.3.2 ### Fixes and improvements @@ -71,7 +81,7 @@ - Fix event code length validation (min: 5, max: 10) - Fix presentation upload progress when editing an event -### v.2.3.1 +## v.2.3.1 ### Fixes and improvements @@ -82,7 +92,7 @@ - Add option to force login to submit quizzes - Fix url with question mark being flagged as a question -### v.2.3.0 +## v.2.3.0 ### Features diff --git a/assets/package-lock.json b/assets/package-lock.json index 4365d3b..26b2fd1 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "assets", "dependencies": { "@sjmc11/tourguidejs": "^0.0.16", "air-datepicker": "^3.5.0", @@ -1093,9 +1094,10 @@ "license": "ISC" }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1405,9 +1407,9 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -1417,22 +1419,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -1499,9 +1485,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1510,9 +1497,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -1615,17 +1602,16 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { @@ -2245,9 +2231,9 @@ "dev": true }, "immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==" }, "is-binary-path": { "version": "2.1.0", @@ -2386,20 +2372,14 @@ "dev": true }, "minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "requires": { "minipass": "^7.1.2" } }, - "mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true - }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -2459,14 +2439,14 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" }, "postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "requires": { "nanoid": "^3.3.11", @@ -2533,16 +2513,15 @@ "dev": true }, "tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, diff --git a/lib/claper_web/live/event_live/form_component.ex b/lib/claper_web/live/event_live/form_component.ex index b601e45..80ce149 100644 --- a/lib/claper_web/live/event_live/form_component.ex +++ b/lib/claper_web/live/event_live/form_component.ex @@ -61,7 +61,7 @@ defmodule ClaperWeb.EventLive.FormComponent do form={f} labelClass="text-white" fieldClass="bg-gray-700 text-white" - key={safe_field_atom(field.name)} + key={field_key(field.name)} name={field.name} required={field.required} value={ @@ -75,7 +75,7 @@ defmodule ClaperWeb.EventLive.FormComponent do form={f} labelClass="text-white" fieldClass="bg-gray-700 text-white" - key={safe_field_atom(field.name)} + key={field_key(field.name)} name={field.name} required={field.required} value={ @@ -176,13 +176,12 @@ defmodule ClaperWeb.EventLive.FormComponent do end end - defp safe_field_atom(name) when is_binary(name) do - String.to_existing_atom(name) - rescue - ArgumentError -> - if Regex.match?(~r/^[\w]{1,100}$/, name), - do: String.to_atom(name), - else: :invalid_field + defp field_key(name) when is_binary(name) do + try do + String.to_existing_atom(name) + rescue + ArgumentError -> String.to_atom(name) + end end def toggle_form(js \\ %JS{}) do diff --git a/mix.exs b/mix.exs index cca206a..d02e7ba 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Claper.MixProject do use Mix.Project - @version "2.5.0" + @version "2.5.1" def project do [ diff --git a/test/claper_web/controllers/stat_controller_test.exs b/test/claper_web/controllers/stat_controller_test.exs new file mode 100644 index 0000000..d74952c --- /dev/null +++ b/test/claper_web/controllers/stat_controller_test.exs @@ -0,0 +1,62 @@ +defmodule ClaperWeb.StatControllerTest do + use ClaperWeb.ConnCase, async: true + + import Claper.{AccountsFixtures, FormsFixtures, PresentationsFixtures} + + describe "POST /export/forms/:form_id" do + setup %{conn: conn} do + owner = confirmed_user_fixture() + presentation_file = presentation_file_fixture(%{user: owner}, [:event]) + + form = + form_fixture(%{ + presentation_file_id: presentation_file.id, + title: "My Form", + fields: [ + %{name: "First Name", type: "text", required: false}, + %{name: "Email Address", type: "email", required: false} + ] + }) + + %{conn: log_in_user(conn, owner), form: form, event: presentation_file.event} + end + + test "exports CSV with field names containing spaces as headers and values", %{ + conn: conn, + form: form, + event: event + } do + {:ok, _submit} = + Claper.Forms.create_or_update_form_submit(event.uuid, %{ + "attendee_identifier" => "attendee-1", + "form_id" => form.id, + "response" => %{"First Name" => "Ada", "Email Address" => "ada@example.com"} + }) + + conn = post(conn, ~p"/export/forms/#{form.id}") + + assert response_content_type(conn, :csv) + body = response(conn, 200) + + [header_line, data_line | _] = String.split(body, "\r\n", trim: true) + + assert header_line =~ "First Name" + assert header_line =~ "Email Address" + + # The CSV row must contain the values, proving header→response key + # alignment works for space-containing field names. Regression: when + # submissions were saved under a wrong key, the CSV row was empty. + assert data_line =~ "Ada" + assert data_line =~ "ada@example.com" + end + + test "returns 403 for users who don't own the event", %{form: form} do + stranger = confirmed_user_fixture() + conn = build_conn() |> log_in_user(stranger) + + conn = post(conn, ~p"/export/forms/#{form.id}") + + assert response(conn, 403) == "Forbidden" + end + end +end diff --git a/test/claper_web/live/event_live/form_component_test.exs b/test/claper_web/live/event_live/form_component_test.exs new file mode 100644 index 0000000..a89b0d8 --- /dev/null +++ b/test/claper_web/live/event_live/form_component_test.exs @@ -0,0 +1,72 @@ +defmodule ClaperWeb.EventLive.FormComponentTest do + use ClaperWeb.ConnCase, async: true + + import Phoenix.LiveViewTest + + import Claper.{AccountsFixtures, FormsFixtures, PresentationsFixtures} + + alias ClaperWeb.EventLive.FormComponent + + defp setup_form(field_attrs) do + presentation_file = presentation_file_fixture(%{}, [:event]) + + form = + form_fixture(%{ + presentation_file_id: presentation_file.id, + fields: field_attrs + }) + + %{event: presentation_file.event, form: form} + end + + describe "rendering fields with non-alphanumeric names" do + test "renders an input whose name uses the original field name (with spaces)" do + %{event: event, form: form} = + setup_form([ + %{name: "First Name", type: "text", required: false}, + %{name: "Email Address", type: "email", required: false} + ]) + + html = + render_component(FormComponent, + id: "form-component", + form: form, + current_user: user_fixture(), + attendee_identifier: nil, + event: event, + current_form_submit: nil + ) + + # The HTML name attribute must contain the actual field name — + # regression: a previous version mapped any non-\w name to :invalid_field, + # so all space-containing fields collided on `form_submit[invalid_field]` + # and submissions were saved under the wrong key. + assert html =~ ~s(name="form_submit[First Name]") + assert html =~ ~s(name="form_submit[Email Address]") + refute html =~ "invalid_field" + end + + test "preserves submitted values when re-rendering for fields with spaces" do + %{event: event, form: form} = + setup_form([ + %{name: "First Name", type: "text", required: false} + ]) + + form_submit = %Claper.Forms.FormSubmit{ + response: %{"First Name" => "Ada"} + } + + html = + render_component(FormComponent, + id: "form-component", + form: form, + current_user: user_fixture(), + attendee_identifier: nil, + event: event, + current_form_submit: form_submit + ) + + assert html =~ ~s(value="Ada") + end + end +end