Skip to content

Commit 8c22b14

Browse files
refact!: Replace v-html with v-safe-html
* feat: `ModelWithContent::toSafeHtmlString()` * refact: Use `::toSafeHtmlString` for field options * refact: Use `v-safe-html` for `k-item` * refact: Use `v-safe-html` for `k-tags` * refact: Use `v-safe-html` for `k-text` * refact: Migrate `html: true` to `v-safe-html` * refact: Use `HtmlString` in dialog controllers * test: Ensure no `v-html` is added easily * fix: Wrap remaining i18n strings as trusted * chore: Upgrade `slint-plugin-vuejs-accessibility` * feat: `HasI18n::i18nHtml()` * docs: Fix inline code comments * Update src/Toolkit/HtmlString.php Co-authored-by: Lukas Bestle <lukas@getkirby.com> --------- Co-authored-by: Lukas Bestle <lukas@getkirby.com>
1 parent 1c4cbd6 commit 8c22b14

125 files changed

Lines changed: 952 additions & 400 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/sections/info.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use Kirby\Toolkit\HtmlString;
34
use Kirby\Toolkit\I18n;
45

56
return [
@@ -22,7 +23,7 @@
2223
if ($this->text) {
2324
$text = $this->model()->toSafeString($this->text);
2425
$text = $this->kirby()->kirbytext($text);
25-
return $text;
26+
return new HtmlString($text);
2627
}
2728
},
2829
],

panel/eslint.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ export default [
8787
},
8888
{
8989
rules: {
90+
"vuejs-accessibility/heading-has-content": [
91+
"error",
92+
{ accessibleDirectives: ["safe-html"] }
93+
],
9094
"vue/attributes-order": "error",
95+
"vue/no-v-html": "error",
9196
"vue/component-definition-name-casing": "off",
9297
"vue/html-closing-bracket-newline": [
9398
"error",

panel/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

panel/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"eslint": "^10.7.0",
4141
"eslint-config-prettier": "^10.1.8",
4242
"eslint-plugin-vue": "^10.9.2",
43-
"eslint-plugin-vuejs-accessibility": "^2.5.0",
43+
"eslint-plugin-vuejs-accessibility": "^2.6.0",
4444
"glob": "^13.0.6",
4545
"globals": "^17.7.0",
4646
"happy-dom": "^20.10.6",

panel/src/components/Collection/Collection.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
</component>
4141

4242
<footer v-if="help || hasPagination" class="k-collection-footer">
43-
<k-text class="k-help k-collection-help" :html="help" />
43+
<k-text class="k-help k-collection-help" :text="help" />
4444
<!--
4545
Emitted when the pagination changes
4646
@event paginate

panel/src/components/Collection/Item.test.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it, vi } from "@test/unit";
22
import { mount as vueMount } from "@vue/test-utils";
3+
import html from "@/panel/html";
34
import Item from "./Item.vue";
45

56
function mount(props = {}, attrs = {}) {
@@ -85,8 +86,15 @@ describe("Item.vue", () => {
8586
});
8687

8788
describe("text prop", () => {
88-
it("renders the text as HTML", () => {
89+
it("escapes a plain string", () => {
8990
const wrapper = mount({ text: "<b>Hello</b>" });
91+
expect(wrapper.find(".k-item-title span").html()).toBe(
92+
"<span>&lt;b&gt;Hello&lt;/b&gt;</span>"
93+
);
94+
});
95+
96+
it("renders trusted HTML as-is", () => {
97+
const wrapper = mount({ text: html("<b>Hello</b>") });
9098
expect(wrapper.find(".k-item-title span").html()).toBe(
9199
"<span><b>Hello</b></span>"
92100
);
@@ -177,18 +185,27 @@ describe("Item.vue", () => {
177185
);
178186
});
179187

180-
it("strips tags", () => {
188+
it("keeps markup a plain string renders as visible text", () => {
181189
const wrapper = mount({ text: "<b>Hello</b>" });
182-
expect(wrapper.find(".k-item-title").attributes("title")).toBe("Hello");
190+
expect(wrapper.find(".k-item-title").attributes("title")).toBe(
191+
"<b>Hello</b>"
192+
);
183193
});
184194

185-
it("unescapes entities", () => {
186-
const wrapper = mount({ text: "Tom &amp; Jerry" });
195+
it("strips authored tags from trusted HTML", () => {
196+
const wrapper = mount({ text: html("<b>Tom &amp; Jerry</b>") });
187197
expect(wrapper.find(".k-item-title").attributes("title")).toBe(
188198
"Tom & Jerry"
189199
);
190200
});
191201

202+
it("keeps escaped tags, which render as visible text", () => {
203+
const wrapper = mount({ text: html("Using &lt;div&gt; tags") });
204+
expect(wrapper.find(".k-item-title").attributes("title")).toBe(
205+
"Using <div> tags"
206+
);
207+
});
208+
192209
it("trims surrounding whitespace", () => {
193210
const wrapper = mount({ text: " Kirby " });
194211
expect(wrapper.find(".k-item-title").attributes("title")).toBe("Kirby");

panel/src/components/Collection/Item.vue

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@
3333
:target="target"
3434
:to="link"
3535
>
36-
<!-- eslint-disable-next-line vue/no-v-html -->
37-
<span v-html="text ?? '&nbsp;'" />
36+
<span v-safe-html="text ?? '\u00a0'" />
3837
</k-link>
39-
<!-- eslint-disable-next-line vue/no-v-html -->
40-
<span v-else v-html="text ?? '&nbsp;'" />
38+
<span v-else v-safe-html="text ?? '\u00a0'" />
4139
</h3>
42-
<!-- eslint-disable-next-line vue/no-v-html -->
43-
<p v-if="info" :title="title(info)" class="k-item-info" v-html="info" />
40+
<p
41+
v-if="info"
42+
v-safe-html="info"
43+
:title="title(info)"
44+
class="k-item-info"
45+
/>
4446
</div>
4547

4648
<div
@@ -81,6 +83,7 @@
8183
<script>
8284
import { props as ItemImageProps } from "./ItemImage.vue";
8385
import { layout } from "@/mixins/props.js";
86+
import { HtmlString } from "@/panel/html";
8487
/**
8588
* A collection item that can be displayed in various layouts
8689
*
@@ -182,9 +185,16 @@ export default {
182185
this.$emit("option", event);
183186
},
184187
title(text) {
185-
return this.$helper.string
186-
.stripHTML(this.$helper.string.unescapeHTML(text))
187-
.trim();
188+
// only trusted HTML carries tags and entities to undo,
189+
// a plain string is already the text we want. Tags go
190+
// first, so escaped ones survive as visible characters.
191+
if (text instanceof HtmlString) {
192+
return this.$helper.string
193+
.unescapeHTML(this.$helper.string.stripHTML(text))
194+
.trim();
195+
}
196+
197+
return String(text ?? "").trim();
188198
}
189199
}
190200
};

panel/src/components/Dialogs/Elements/Text.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<k-text v-if="text" :html="text" />
2+
<k-text v-if="text" :text="text" />
33
<k-box v-else theme="info">{{ empty }}</k-box>
44
</template>
55

panel/src/components/Dialogs/LicenseDialog.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@
4545
<k-definition v-if="license.info" :term="$t('status')">
4646
<p :data-theme="license.theme">
4747
<k-icon :type="license.icon" />
48-
<k-text :html="license.info" />
48+
<k-text :text="license.info" />
4949
</p>
5050
</k-definition>
5151
</k-definitions>
5252

53-
<k-text class="k-help" :html="licenseHubText" />
53+
<k-text class="k-help" :text="licenseHubText" />
5454
</k-stack>
5555
</k-dialog>
5656
</template>
@@ -80,9 +80,11 @@ export default {
8080
emits: ["cancel", "submit"],
8181
computed: {
8282
licenseHubText() {
83-
return this.$t("license.manage.hub", {
84-
url: "https://hub.getkirby.com/"
85-
});
83+
return this.$panel.html(
84+
this.$t("license.manage.hub", {
85+
url: "https://hub.getkirby.com/"
86+
})
87+
);
8688
}
8789
}
8890
};

panel/src/components/Drawers/Elements/Text.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<k-text v-if="text" :html="text" />
2+
<k-text v-if="text" :text="text" />
33
<k-box v-else theme="info">{{ empty }}</k-box>
44
</template>
55

0 commit comments

Comments
 (0)