mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-19 16:18:11 +00:00
chore: re-enable and extend unescaped-html-literal eslint rule (#39832)
https://github.com/go-gitea/gitea/pull/38046 accidentally dropped `gitea/unescaped-html-literal` from the eslint config, so it has been off since. Raised in https://github.com/go-gitea/gitea/pull/39829#issuecomment-6246720235. - Re-enable the rule, exempting e2e tests like unit tests - Detect tags anywhere in strings and template chunks, not only at the start - Allow `htmlRaw` templates, they escape like `html` - Fix the existing violations, which escapes the heatmap tooltip and shortcut keys
This commit is contained in:
@@ -311,6 +311,7 @@ export default defineConfig([
|
||||
'func-names': [0],
|
||||
'func-style': [0],
|
||||
'getter-return': [2],
|
||||
'gitea/unescaped-html-literal': [2],
|
||||
'grouped-accessor-pairs': [2],
|
||||
'guard-for-in': [0],
|
||||
'id-denylist': [0],
|
||||
@@ -1239,6 +1240,9 @@ export default defineConfig([
|
||||
{
|
||||
files: ['*', 'tools/**/*', 'tests/**/*'],
|
||||
languageOptions: {globals: globals.nodeBuiltin},
|
||||
rules: {
|
||||
'gitea/unescaped-html-literal': [0],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web_src/**/*'],
|
||||
|
||||
@@ -28,6 +28,10 @@ ruleTester.run('unescaped-html-literal', rule, {
|
||||
code: 'const helloTemplate = (name) => html`<div>Hello ${name}!</div>`;',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
{
|
||||
code: 'const attr = htmlRaw` width="${width}"`, img = htmlRaw`<img ${attr}>`, cmp = "a < b";',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
@@ -66,6 +70,21 @@ ruleTester.run('unescaped-html-literal', rule, {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const rowHTML = `${cells}</tr>`, tailHTML = '</tbody>', textHTML = 'Hello <b>World</b>'",
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const helloHTML = foo`<div>Hello ${name}!</div>`',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// https://github.com/github/eslint-plugin-github/blob/main/lib/rules/unescaped-html-literal.js
|
||||
import type {JSRuleDefinition, JSRuleDefinitionTypeOptions} from 'eslint';
|
||||
|
||||
const htmlOpenTag = /^\s*<[a-zA-Z]/;
|
||||
const htmlTag = /<\/?[a-z][\w-]*[\s/>]/i;
|
||||
const escapingTags = new Set(['html', 'htmlRaw']);
|
||||
|
||||
const rule: JSRuleDefinition<JSRuleDefinitionTypeOptions> = {
|
||||
meta: {
|
||||
@@ -14,7 +15,7 @@ const rule: JSRuleDefinition<JSRuleDefinitionTypeOptions> = {
|
||||
|
||||
create: (context) => ({
|
||||
Literal(node) {
|
||||
if (typeof node.value !== 'string' || !htmlOpenTag.test(node.value)) return;
|
||||
if (typeof node.value !== 'string' || !htmlTag.test(node.value)) return;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
@@ -22,11 +23,10 @@ const rule: JSRuleDefinition<JSRuleDefinitionTypeOptions> = {
|
||||
});
|
||||
},
|
||||
TemplateLiteral(node) {
|
||||
const templateStart = node.quasis[0]?.value.raw;
|
||||
if (!templateStart || !htmlOpenTag.test(templateStart)) return;
|
||||
if (node.quasis.every((quasi) => !htmlTag.test(quasi.value.raw))) return;
|
||||
|
||||
const parent = node.parent;
|
||||
if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && parent.tag.name === 'html') return;
|
||||
if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && escapingTags.has(parent.tag.name)) return;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {computed, onBeforeUnmount, onMounted} from 'vue';
|
||||
import tippy, {createSingleton} from 'tippy.js';
|
||||
import type {CreateSingletonInstance, Instance} from 'tippy.js';
|
||||
import {getCurrentLocale} from '../utils.ts';
|
||||
import {html} from '../utils/html.ts';
|
||||
|
||||
type HeatmapValue = {date: Date; count: number};
|
||||
type HeatmapCell = {date: Date; colorIndex: number; ariaLabel: string; tooltip: string};
|
||||
@@ -96,7 +97,7 @@ const grid = computed(() => {
|
||||
date: new Date(cursor),
|
||||
colorIndex: hit ? hit.colorIndex : 0,
|
||||
ariaLabel: `${head} ${on} ${dateStr}`,
|
||||
tooltip: `<b>${head}</b> ${on} ${dateStr}`,
|
||||
tooltip: html`<b>${head}</b> ${on} ${dateStr}`,
|
||||
});
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
|
||||
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.classList.add('markup-content-iframe', 'tw-invisible');
|
||||
iframe.srcdoc = '<style>body { margin: 0; overflow: hidden; } #mermaid { display: block; margin: 0 auto; }</style>';
|
||||
iframe.srcdoc = html`<style>body { margin: 0; overflow: hidden; } #mermaid { display: block; margin: 0 auto; }</style>`;
|
||||
const viewBoxHeight = svgNode.viewBox.baseVal?.height;
|
||||
if (viewBoxHeight) iframe.style.height = `${Math.max(Math.ceil(viewBoxHeight), 85)}px`; // min-height keeps the buttons from overlapping
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ function initDevtestAnsiRender(container: HTMLElement) {
|
||||
...withSource(`first${esc}[Ksecond${esc}[2Jthird`),
|
||||
...withSource(`cursor ${esc}[3Amovement, private ${esc}[?25lCSI, ${esc}]0;title${esc}\\titles, ${esc}Pquery${esc}\\strings, truncated${esc}[38;5;`),
|
||||
...withSource('Reading... 10%\b\b\b100%'),
|
||||
...withSource('<script>alert(1)</script> & "quotes", and a bare url https://example.com'),
|
||||
...withSource('<script>alert(1)</script> & "quotes", and a bare url https://example.com'), // eslint-disable-line gitea/unescaped-html-literal -- plain text that the console must escape
|
||||
' ',
|
||||
`${esc}[31man unterminated color`,
|
||||
'carries into the following lines',
|
||||
|
||||
@@ -108,7 +108,7 @@ function showShortcutHelp() {
|
||||
seenShortcuts.add(shortcutIdentifier);
|
||||
let keysHtml = '';
|
||||
for (const keys of shortcutKeys.split(' ')) {
|
||||
keysHtml += `<kbd>${keys}</kbd> `;
|
||||
keysHtml += html`<kbd>${keys}</kbd> `;
|
||||
}
|
||||
list.append(createElementFromHTML(html`<tr><td>${htmlRaw(keysHtml)}</td><td>${label}</td></tr>`));
|
||||
}
|
||||
|
||||
@@ -166,8 +166,7 @@ function attachTooltip(target: Element, content: Content | null = null): Instanc
|
||||
|
||||
let allowHTML = target.getAttribute('data-tooltip-render') === 'html';
|
||||
if (!allowHTML && typeof content === 'string') {
|
||||
content = htmlEscape(content);
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
content = htmlEscape(content).split('\n').join(html`<br>`);
|
||||
allowHTML = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user