From 4a9fcf381b91df2c23b16ad1a71f023c84c0f1de Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Fri, 20 Feb 2026 09:46:21 +0100 Subject: [PATCH 1/3] fix: add validation on editor image form to avoid special characters --- config/application.rb | 1 + .../decidim/editor_image_form_extends.rb | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 lib/extends/forms/decidim/editor_image_form_extends.rb diff --git a/config/application.rb b/config/application.rb index 16b8fd27f2..4f8f7af890 100644 --- a/config/application.rb +++ b/config/application.rb @@ -29,6 +29,7 @@ class Application < Rails::Application require "extends/forms/decidim/comments/comment_form_extends" require "extends/forms/decidim/system/base_organization_form_extends" require "extends/forms/decidim/omniauth_registration_form_extends" + require "extends/forms/decidim/editor_image_form_extends" # controllers require "extends/controllers/decidim/admin/scopes_controller_extends" require "extends/controllers/decidim/scopes_controller_extends" diff --git a/lib/extends/forms/decidim/editor_image_form_extends.rb b/lib/extends/forms/decidim/editor_image_form_extends.rb new file mode 100644 index 0000000000..752f04659e --- /dev/null +++ b/lib/extends/forms/decidim/editor_image_form_extends.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module EditorImageFormExtends + extend ActiveSupport::Concern + + included do + validate :no_special_character_in_file_name + + # do not allow special characters like accents, spaces..except dash in image filename + # added to avoid broken images in proposals custom fields rich text editor + def no_special_character_in_file_name + if /\W/=~ file.original_filename.split(".").first + errors.add :file, :invalid + end + end + end +end + +Decidim::EditorImageForm.include(EditorImageFormExtends) From d003d68c3fdc789356cede7164f210b653a7ca8b Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Fri, 20 Feb 2026 09:47:03 +0100 Subject: [PATCH 2/3] fix: add javascript to handle errors on editor form --- app/packs/src/decidim/decidim_application.js | 87 ++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/app/packs/src/decidim/decidim_application.js b/app/packs/src/decidim/decidim_application.js index 5d5dcf59f4..39b5a4284b 100644 --- a/app/packs/src/decidim/decidim_application.js +++ b/app/packs/src/decidim/decidim_application.js @@ -3,3 +3,90 @@ // Load images require.context("../../images", true) + +// add an error message if loading image with special character in rich-text editor +document.addEventListener("DOMContentLoaded", function(event) { + setTimeout(function (){ + const container = document.querySelector('div.proposal_custom_field div.editor-container[type=richtext]') || document.querySelector('div.editor div.editor-container'); + const editorToolbarImage = document.querySelector('div.editor-toolbar button[data-editor-type="image"]'); + + if(container && editorToolbarImage){ + // add paragraph before container + const paragraph = document.createElement('p'); + paragraph.style.fontSize = "14px"; + paragraph.style.textAlign = "justify"; + paragraph.style.color = "rgb(62 76 92 / var(--tw-text-opacity, 1))"; + + const lang = document.querySelector('html').lang + let text; + switch (lang) { + case "fr": text = "Si vous ajoutez une image, le nom du fichier ne doit pas contenir de caractères spéciaux (espace, accent, parenthèse...)."; + break; + case "de": text = "Wenn Sie ein Bild hinzufügen, darf der Dateiname keine Sonderzeichen (Leerzeichen, Akzente, Klammern...) enthalten."; + break; + case "nl": text = "Als je een afbeelding toevoegt, mag de bestandsnaam geen speciale tekens bevatten (spaties, accenten, haakjes...)."; + break; + default: text = "If you upload an image, the name of the file must not contain special characters (space, accent, parenthesis...)."; + } + paragraph.textContent = text; + container.before(paragraph); + + // add guidance to modal + const modalText = document.querySelector('div.upload-modal .upload-modal__text ul'); + const li = document.createElement('li'); + let liText; + switch (lang) { + case "fr": liText = "Pas de caractères spéciaux dans le nom de l'image."; + break; + case "de": liText = "Keine Sonderzeichen im Bildnamen."; + break; + case "nl": liText = "Geen speciale tekens in de afbeeldingsnaam."; + break; + default: liText = "No special characters in image name."; + } + li.textContent = liText; + modalText.appendChild(li); + + function showEditorError(message) { + // Remove previous existing error if exists + const existingError = document.querySelector("div.custom-editor-upload-error"); + if (existingError) { + existingError.remove(); + } + + const errorDiv = document.createElement("div"); + errorDiv.classList.add("custom-editor-upload-error", "form-error", "is-visible"); + errorDiv.textContent = message; + + let div = document.querySelector('div.proposal_custom_field div.editor-container[type=richtext]') || document.querySelector('div.editor div.editor-container'); + // Insert error after container + div.after(errorDiv); + } + + const originalFetch = window.fetch; + + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + // target only calls to editor_images endpoint + const url = typeof args[0] === "string" ? args[0] : args[0]?.url; + if (url && url.includes("editor_images")) { + // if EditorForm is invalid, there is a 422 + if (response.status === 422) { + // Clone response to read it without consuming it + const cloned = response.clone(); + cloned.json().then((data) => { + showEditorError(data.message); + }); + } else if (response.ok) { + // Remove previous existing error if upload is a success + const existingError = document.querySelector(".custom-editor-upload-error"); + if (existingError) { + existingError.remove(); + } + } + } + return response; + }; + } + }, 500); +}); From c96d7d6049177521c8c2e18a80260fe51bd2b1a7 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Fri, 20 Feb 2026 11:48:37 +0100 Subject: [PATCH 3/3] test: add tests --- spec/factories.rb | 1 + spec/forms/editor_image_form_spec.rb | 117 +++++++++++++++++++ spec/system/new_proposal_and_editor_spec.rb | 118 ++++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 spec/forms/editor_image_form_spec.rb create mode 100644 spec/system/new_proposal_and_editor_spec.rb diff --git a/spec/factories.rb b/spec/factories.rb index 7eefa0baa9..8ad293e307 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -7,3 +7,4 @@ require "decidim/surveys/test/factories" require "decidim/budgets/test/factories" require "decidim/initiatives/test/factories" +require "decidim/decidim_awesome/test/factories" diff --git a/spec/forms/editor_image_form_spec.rb b/spec/forms/editor_image_form_spec.rb new file mode 100644 index 0000000000..adcbd8fe47 --- /dev/null +++ b/spec/forms/editor_image_form_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe EditorImageForm do + subject { described_class.from_params(attributes).with_context(context) } + + let(:attributes) do + { + "editor_image" => { + organization:, + author_id: user_id, + file: + } + } + end + let(:context) do + { + current_organization: organization, + current_user: user + } + end + let(:user) { create(:user, :admin, :confirmed) } + let(:organization) { user.organization } + let(:user_id) { user.id } + let(:file) { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } + + context "with correct data" do + it "is valid" do + expect(subject).to be_valid + end + end + + context "with an empty user_id" do + let(:user_id) { nil } + + it "is invalid" do + expect(subject).not_to be_valid + end + end + + context "with an empty organization" do + let(:organization) { nil } + + it "is invalid" do + expect(subject).not_to be_valid + end + end + + context "when images are not the expected type" do + let(:file) { Decidim::Dev.test_file("Exampledocument.pdf", "application/pdf") } + + it { is_expected.not_to be_valid } + end + + context "when the original filename of the image has an underscore" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("app/packs/images/FranceConnect-Bouton/01-Principal/png/franceconnect-btn-principal.png"), + "image/png", + original_filename: "test_image.jpg" + ) + end + + it { is_expected.to be_valid } + end + + context "when the original filename of the image has a dash" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("app/packs/images/FranceConnect-Bouton/01-Principal/png/franceconnect-btn-principal.png"), + "image/png", + original_filename: "test-image.jpg" + ) + end + + it { is_expected.not_to be_valid } + end + + context "when the original filename of the image has an accent" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("app/packs/images/FranceConnect-Bouton/01-Principal/png/franceconnect-btn-principal.png"), + "image/png", + original_filename: "imàge.jpg" + ) + end + + it { is_expected.not_to be_valid } + end + + context "when the original filename of the image has a space" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("app/packs/images/FranceConnect-Bouton/01-Principal/png/franceconnect-btn-principal.png"), + "image/png", + original_filename: "test image.jpg" + ) + end + + it { is_expected.not_to be_valid } + end + + context "when the original filename of the image has a parenthesis" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("app/packs/images/FranceConnect-Bouton/01-Principal/png/franceconnect-btn-principal.png"), + "image/png", + original_filename: "test(image.jpg" + ) + end + + it { is_expected.not_to be_valid } + end + end +end diff --git a/spec/system/new_proposal_and_editor_spec.rb b/spec/system/new_proposal_and_editor_spec.rb new file mode 100644 index 0000000000..bae543b179 --- /dev/null +++ b/spec/system/new_proposal_and_editor_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Proposals" do + include_context "with a component" + let(:manifest_name) { "proposals" } + let!(:user) { create(:user, :confirmed, organization:) } + let(:images_editor) { false } + let!(:allow_images_in_editors) { create(:awesome_config, organization:, var: :allow_images_in_editors, value: images_editor) } + let!(:component) do + create(:proposal_component, + :with_creation_enabled, + manifest:, + participatory_space: participatory_process, + settings: { new_proposal_body_template: body_template }) + end + let(:body_template) do + { "en" => "

This test has many characters

" } + end + + context "when creating a new proposal" do + before do + login_as user, scope: :user + visit_component + end + + context "when rich text editor is enabled for participants but images in rich text are not allowed" do + before do + organization.update(rich_text_editor_in_public_views: true) + click_on "New proposal" + end + + it_behaves_like "having a rich text editor", "new_proposal", "basic" + + it "has helper character counter" do + within "form.new_proposal" do + within ".editor .input-character-counter__text" do + expect(page).to have_content("At least 15 characters", count: 1) + end + end + end + + it "displays the text with rich text in the input body" do + within "form.new_proposal" do + within ".editor-input" do + expect(find("p").text).to eq("This test has many characters") + expect(find("strong").text).to eq("many") + end + end + end + + it "does not display a text above the editor to avoid special characters in image name" do + within "div.editor" do + expect(page).to have_no_content("If you upload an image, the name of the file must not contain special characters (space, accent, parenthesis...).") + end + end + + context "and images are allowed in rich text" do + let(:images_editor) { true } + let(:editor_selector) { ".editor-input" } + let(:image) { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } + + it "displays a text above the editor to avoid special characters in image name" do + within "div.editor" do + expect(page).to have_content("If you upload an image, the name of the file must not contain special characters (space, accent, parenthesis...).") + end + end + + it "displays a text in the upload modal to avoid special characters in image name" do + find('.editor-toolbar-control[data-editor-type="image"]').click + + within "div.upload-modal__text" do + expect(page).to have_content("No special characters in image name.") + end + end + end + end + + describe "validating the form" do + before do + click_on "New proposal" + end + + context "when focus shifts to body" do + it "displays error when title is empty" do + fill_in :proposal_title, with: " " + find_by_id("proposal_body").click + + expect(page).to have_css(".form-error.is-visible", text: "There is an error in this field.") + end + + it "displays error when title is invalid" do + fill_in :proposal_title, with: "invalid-title" + find_by_id("proposal_body").click + + expect(page).to have_css(".form-error.is-visible", text: "There is an error in this field") + end + end + + context "when focus remains on title" do + it "does not display error when title is empty" do + fill_in :proposal_title, with: " " + find_by_id("proposal_title").click + + expect(page).to have_no_css(".form-error.is-visible", text: "There is an error in this field.") + end + + it "does not display error when title is invalid" do + fill_in :proposal_title, with: "invalid-title" + find_by_id("proposal_title").click + + expect(page).to have_no_css(".form-error.is-visible", text: "There is an error in this field") + end + end + end + end +end