diff --git a/src/ui/src/components/core/other/CoreHtml.spec.ts b/src/ui/src/components/core/other/CoreHtml.spec.ts new file mode 100644 index 000000000..a4ff454b7 --- /dev/null +++ b/src/ui/src/components/core/other/CoreHtml.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; + +import CoreHtml from "./CoreHtml.vue"; +import { flushPromises, mount } from "@vue/test-utils"; +import injectionKeys from "@/injectionKeys"; +import { ref } from "vue"; +import { mockProvides } from "@/tests/mocks"; + +describe("CoreCheckboxInput", () => { + it("should filter invalid Attributes props", async () => { + const wrapper = mount(CoreHtml, { + slots: { + default: "slot", + }, + global: { + provide: { + ...mockProvides, + [injectionKeys.evaluatedFields as symbol]: { + htmlInside: ref("inside"), + element: ref("div"), + styles: ref({ + color: "red", + }), + attrs: ref({ + "0invalid": "invalid", + valid: "valid", + }), + }, + }, + }, + }); + + await flushPromises(); + + const attrs = wrapper.attributes(); + + expect(attrs.valid).toBe("valid"); + expect(attrs.invalid).toBeUndefined(); + expect(attrs.style).toBe("color: red;"); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/src/ui/src/components/core/other/CoreHtml.vue b/src/ui/src/components/core/other/CoreHtml.vue index 538c7a64c..51b447183 100644 --- a/src/ui/src/components/core/other/CoreHtml.vue +++ b/src/ui/src/components/core/other/CoreHtml.vue @@ -6,7 +6,7 @@ All valid HTML tags are supported, including tags such as \`iframe\`, allowing y Take into account the potential risks of adding custom HTML to your app, including XSS. Be specially careful when injecting user-generated data. </docs> <script lang="ts"> -import { h, inject } from "vue"; +import { computed, h, inject } from "vue"; import { FieldControl, FieldType } from "@/writerTypes"; import injectionKeys from "@/injectionKeys"; import { cssClasses } from "@/renderer/sharedStyleFields"; @@ -39,7 +39,7 @@ export default { }, styles: { name: "Styles", - default: null, + default: "{}", init: JSON.stringify(defaultStyle, null, 2), type: FieldType.Object, desc: "Define the CSS styles to apply to the HTML element using a JSON object or a state reference to a dictionary.", @@ -47,7 +47,7 @@ export default { }, attrs: { name: "Attributes", - default: null, + default: "{}", type: FieldType.Object, desc: "Set additional HTML attributes for the element using a JSON object or a dictionary via a state reference.", validator: validatorObjectRecordNotNested, @@ -64,6 +64,17 @@ export default { }, setup(_, { slots }) { const fields = inject(injectionKeys.evaluatedFields); + + const validAttributeNameRegex = /^[a-zA-Z][a-zA-Z0-9-_:.]*$/; + + const attrs = computed(() => { + // filter invalid attribute keys + const entries = Object.entries(fields.attrs.value).filter(([key]) => + validAttributeNameRegex.test(key), + ); + return Object.fromEntries(entries); + }); + return () => { let insideHtmlNode = undefined; if (fields.htmlInside.value) { @@ -71,10 +82,11 @@ export default { innerHTML: fields.htmlInside.value, }); } + return h( fields.element.value, { - ...fields.attrs.value, + ...attrs.value, class: "CoreHTML", "data-writer-container": "", style: fields.styles.value, diff --git a/src/ui/src/components/core/other/__snapshots__/CoreHtml.spec.ts.snap b/src/ui/src/components/core/other/__snapshots__/CoreHtml.spec.ts.snap new file mode 100644 index 000000000..64e6085c5 --- /dev/null +++ b/src/ui/src/components/core/other/__snapshots__/CoreHtml.spec.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CoreCheckboxInput > should filter invalid Attributes props 1`] = ` +<div + class="CoreHTML" + data-writer-container="" + style="color: red;" + valid="valid" +> + + slot + + <div> + inside + </div> +</div> +`;