[v14.0/forgejo] Add to html button in markdown type="button" (#10527)

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/10520

This is for preventing that a markdown button is recognized as button for submission in a html form.

Buttons can't be stripped from the markdown due to: https://codeberg.org/forgejo/forgejo/pulls/7670#issuecomment-4086608

There is no issue with buttons if they always have `type="button"`, so this should be fine.

This is a "follow-up" to !7670.

Fixes #7656

Co-authored-by: Beowulf <beowulf@beocode.eu>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10527
Reviewed-by: Beowulf <beowulf@beocode.eu>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
forgejo-backport-action 2025-12-22 00:29:08 +01:00 committed by Gusted
commit 650252f851
5 changed files with 97 additions and 1 deletions

View file

@ -1,4 +1,5 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown
@ -87,6 +88,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
if scope, found := ctx.Metas["scope"]; found {
v.Name = fmt.Appendf(v.Name, "-%s", scope)
}
case *ast.RawHTML:
g.transformRawHTML(ctx, v, reader)
}
return ast.WalkContinue, nil
})

View file

@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown_test
@ -125,6 +126,32 @@ func TestRender_Images(t *testing.T) {
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
}
func TestRender_Buttons(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
}
test(
"<button>Test</button>",
`<p><button type="button">Test</button></p>`)
test(
`<button class="toggle-escape-button btn interact-bg">Test</button>`,
`<p><button type="button" class="toggle-escape-button btn interact-bg">Test</button></p>`)
test(
`<button type="submit" class="toggle-escape-button btn interact-bg">Test</button>`,
`<p><button type="button" class="toggle-escape-button btn interact-bg">Test</button></p>`)
}
func testAnswers(baseURLContent, baseURLImages string) []string {
return []string{
`<p>Wiki! Enjoy :)</p>

View file

@ -0,0 +1,28 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package markdown
import (
"strings"
"forgejo.org/modules/markup"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
func (g *ASTTransformer) addTypeToButton(v *ast.RawHTML, segment string) {
segment = strings.TrimPrefix(segment, "<button")
newTag := ast.NewString([]byte(`<button type="button"` + segment))
newTag.SetCode(true)
v.Parent().ReplaceChild(v.Parent(), v, newTag)
}
func (g *ASTTransformer) transformRawHTML(_ *markup.RenderContext, v *ast.RawHTML, reader text.Reader) {
segment := string(v.Segments.Value(reader.Source()))
if strings.HasPrefix(segment, "<button") {
g.addTypeToButton(v, segment)
}
}

View file

@ -1,5 +1,6 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2017 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
@ -78,6 +79,9 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
// Buttons
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^button$`)).OnElements("button")
// Custom URL-Schemes
if len(setting.Markdown.CustomURLSchemes) > 0 {
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)

View file

@ -0,0 +1,34 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// modules/markup/**
// web_src/js/features/repo-unicode-escape.js
// @watch end
import {expect} from '@playwright/test';
import {dynamic_id, test} from './utils_e2e.ts';
test.use({user: 'user2'});
test('Escape button in file preview', async ({page}) => {
await page.goto('/user2/unicode-escaping/src/branch/main/a-file');
const url = await page.getByRole('link', {name: 'Permalink'}).getAttribute('href');
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
// Create a new issue
await page.getByPlaceholder('Title').fill(dynamic_id());
await page.getByPlaceholder('Leave a comment').fill(`http://localhost:3003${url}#L1`);
await page.getByRole('button', {name: 'Create issue'}).click();
await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/);
await expect(page.locator('table.file-preview.unicode-escaped')).toHaveCount(0);
await expect(async () => {
await page.locator('button.toggle-escape-button').click();
await expect(page.locator('table.file-preview.unicode-escaped')).toHaveCount(1);
}).toPass();
});