fix: use scrollHeight for rendered iframe if offsetHeight is unavailable (#9508)

Fixes #9421.

Added integration test.

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9508
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Bojidar Marinov <bojidar.marinov.bg@gmail.com>
Co-committed-by: Bojidar Marinov <bojidar.marinov.bg@gmail.com>
This commit is contained in:
Bojidar Marinov 2025-10-16 15:51:57 +02:00 committed by Gusted
parent e56fdf1ec5
commit 8ed95dc4c6
22 changed files with 125 additions and 6 deletions

View file

@ -1921,4 +1921,3 @@
is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false
topics: '[]'

View file

@ -326,7 +326,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
// Append a short script to the iframe's contents, which will communicate the scroll height of the embedded document via postMessage, either once loaded (in case the containing page loads first) in response to a postMessage from external.js, in case the iframe loads first
// We use '*' as a target origin for postMessage, because can be certain we are embedded on the same domain, due to X-Frame-Options configured elsewhere. (Plus, the offsetHeight of an embedded document is likely not sensitive data anyway.)
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight || document.documentElement.scrollHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
}
_ = pw.Close()

View file

@ -69,7 +69,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
username := "user2"
unadoptedList := []string{path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
unadoptedList := []string{path.Join(username, "rendering-test"), path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
for _, unadopted := range unadoptedList {
_ = os.Mkdir(path.Join(setting.RepoRootPath, unadopted+".git"), 0o755)
}
@ -77,13 +77,13 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
opts := db.ListOptions{Page: 1, PageSize: 1}
repoNames, count, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
assert.Equal(t, 2, count)
assert.Equal(t, 3, count)
assert.Equal(t, unadoptedList[0], repoNames[0])
opts = db.ListOptions{Page: 2, PageSize: 1}
repoNames, count, err = ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
assert.Equal(t, 2, count)
assert.Equal(t, 3, count)
assert.Equal(t, unadoptedList[1], repoNames[0])
}
@ -97,7 +97,7 @@ func TestAdoptRepository(t *testing.T) {
path.Join(setting.RepoRootPath, username, unadopted+".git"),
))
opts := db.ListOptions{Page: 1, PageSize: 1}
opts := db.ListOptions{Page: 2, PageSize: 1}
repoNames, _, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
require.Contains(t, repoNames, path.Join(username, unadopted))

View file

@ -0,0 +1,6 @@
-
id: 1001
repo_id: 1002
type: 1
config: "{}"
created_unix: 946684810

View file

@ -11,3 +11,18 @@
status: 0
lfs_size: 8192
topics: '[]'
-
id: 1002
owner_id: 2
owner_name: user2
lower_name: rendering-test
name: rendering-test
default_branch: master
is_empty: false
is_archived: false
is_private: false
status: 0
num_issues: 0
topics: '[]'

View file

@ -0,0 +1,61 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// web_src/js/markup/external.js
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
test('iframe renderer shrinks to shorter page', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/short.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeLessThan(300);
});
test('iframe renderer expands to taller page', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/tall.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeGreaterThan(300);
});
test('iframe renderer expands to taller page with absolutely-positioned body', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/absolute.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeGreaterThan(300);
});
test('iframe renderer remains at default height if script breaks', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/fail.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeCloseTo(300, 0.5);
});

View file

@ -0,0 +1 @@
ref: refs/heads/master

View file

@ -0,0 +1,4 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true

View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View file

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View file

@ -0,0 +1 @@
x+)JMU047g040031QHL*но)-IукL+JлMм(имa╦u╖┼-ЩR╨Эеmгv4ЗЩWqь╪ш╙<-13Y╘iЛ?∙и=ЖlK▌╡Вo⌠z╟╕╙╢8#©╗Y╜Qж╣*rO?тЬ-s╗тo╧ЕUF╗з▓дc╥_J~!\Бx╖Л{╓÷iо√:иsУсK

View file

@ -0,0 +1 @@
fafaad77cb54665ac800d1bf77e6a55bd355eabc

View file

@ -125,3 +125,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false

View file

@ -139,3 +139,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false

View file

@ -126,3 +126,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false