From 484eacb7bfe3de8d8ca002342f63cbe3ff360770 Mon Sep 17 00:00:00 2001 From: OptionalValue <167444167+OptionalValue@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:23:27 +0100 Subject: [PATCH 01/30] fix: /repos/{owner}/{repo}/actions/{runs,jobs} requiring owner permissions (#36818) Resolves #36268 The REST endpoints: `/repos/{owner}/{repo}/actions/runs` `/repos/{owner}/{repo}/actions/jobs` currently require repository/organisation owner permissions, even though in GitHub they only need simple "read" permissions on the repo. In the web interface this is implemented correctly, where anyone with "read" permissions can see the list of action runs. --------- Co-authored-by: Leonard Immel --- routers/api/v1/api.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index cb6bbe0954..767e5533fd 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -895,34 +895,35 @@ func Routes() *web.Router { addActionsRoutes := func( m *web.Router, - reqChecker func(ctx *context.APIContext), + reqReaderCheck func(ctx *context.APIContext), + reqOwnerCheck func(ctx *context.APIContext), act actions.API, ) { m.Group("/actions", func() { m.Group("/secrets", func() { - m.Get("", reqToken(), reqChecker, act.ListActionsSecrets) + m.Get("", reqToken(), reqOwnerCheck, act.ListActionsSecrets) m.Combo("/{secretname}"). - Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret). - Delete(reqToken(), reqChecker, act.DeleteSecret) + Put(reqToken(), reqOwnerCheck, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret). + Delete(reqToken(), reqOwnerCheck, act.DeleteSecret) }) m.Group("/variables", func() { - m.Get("", reqToken(), reqChecker, act.ListVariables) + m.Get("", reqToken(), reqOwnerCheck, act.ListVariables) m.Combo("/{variablename}"). - Get(reqToken(), reqChecker, act.GetVariable). - Delete(reqToken(), reqChecker, act.DeleteVariable). - Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable). - Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable) + Get(reqToken(), reqOwnerCheck, act.GetVariable). + Delete(reqToken(), reqOwnerCheck, act.DeleteVariable). + Post(reqToken(), reqOwnerCheck, bind(api.CreateVariableOption{}), act.CreateVariable). + Put(reqToken(), reqOwnerCheck, bind(api.UpdateVariableOption{}), act.UpdateVariable) }) m.Group("/runners", func() { - m.Get("", reqToken(), reqChecker, act.ListRunners) - m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken) - m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) - m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) + m.Get("", reqToken(), reqOwnerCheck, act.ListRunners) + m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken) + m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner) + m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner) }) - m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns) - m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs) + m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns) + m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs) }) } @@ -1164,7 +1165,8 @@ func Routes() *web.Router { m.Post("/reject", repo.RejectTransfer) }, reqToken()) - addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management + // Adds the routes for secrets/variables and runner management + addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction()) m.Group("/actions/workflows", func() { m.Get("", repo.ActionsListRepositoryWorkflows) @@ -1619,6 +1621,7 @@ func Routes() *web.Router { }) addActionsRoutes( m, + reqOrgMembership(), reqOrgOwnership(), org.NewAction(), ) From b874e0d8e571ca2eaafde72965e6f3adb6ea6073 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 4 Mar 2026 00:47:08 +0000 Subject: [PATCH 02/30] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index ad00325b02..4669252a6e 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -84,6 +84,7 @@ "save": "Sábháil", "add": "Cuir", "add_all": "Cuir Gach", + "dismiss": "Díbhe", "remove": "Bain", "remove_all": "Bain Gach", "remove_label_str": "Bain mír “%s”", @@ -224,7 +225,7 @@ "startpage.lightweight": "Éadrom", "startpage.lightweight_desc": "Tá íosta riachtanais íseal ag Gitea agus is féidir leo rith ar Raspberry Pi saor. Sábháil fuinneamh do mheaisín!", "startpage.license": "Foinse Oscailte", - "startpage.license_desc": "Téigh go bhfaighidh %[2]s! Bí linn trí cur leis chun an tionscadal seo a fheabhsú fós. Ná bíodh cúthail ort a bheith i do rannpháirtí!", + "startpage.license_desc": "Téigh agus faigh %[2]s! Bí linn trí cur leis chun an tionscadal seo a dhéanamh níos fearr fós. Ná bíodh leisce ort cur leis!", "install.install": "Suiteáil", "install.installing_desc": "Suiteáil anois, fan go fóill…", "install.title": "Cumraíocht Tosaigh", @@ -284,12 +285,6 @@ "install.register_confirm": "Deimhniú Ríomhphoist a cheangal le Clárú", "install.mail_notify": "Cumasaigh Fógraí Ríomhphoist", "install.server_service_title": "Socruithe Freastalaí agus Seirbhíse Tríú Páirtí", - "install.offline_mode": "Cumasaigh Mód Áitiúil", - "install.offline_mode_popup": "Díchumasaigh líonraí seachadta ábhair tríú páirtí agus freastal ar na hacmhainní go léir go háitiúil.", - "install.disable_gravatar": "Díchumasaigh Gravatar", - "install.disable_gravatar_popup": "Díchumasaigh foinsí abhatár Gravatar agus tríú páirtí. Úsáidfear abhatár réamhshocraithe mura n-uaslódálann úsáideoir abhatár go háitiúil.", - "install.federated_avatar_lookup": "Cumasaigh Abhatáir Chónaidhme", - "install.federated_avatar_lookup_popup": "Cumasaigh cuardach avatar cónaidhme ag baint úsáide as Libravatar.", "install.disable_registration": "Díchumasaigh Féin-Chlárú", "install.disable_registration_popup": "Díchumasaigh féinchlárú úsáideora. Ní bheidh ach riarthóirí in ann cuntais úsáideora nua a chruthú.", "install.allow_only_external_registration_popup": "Ceadaigh Clárú Trí Sheirbhísí Seachtracha amháin", @@ -871,7 +866,7 @@ "settings.permissions_list": "Ceadanna:", "settings.manage_oauth2_applications": "Bainistigh Feidhmchláir OAuth2", "settings.edit_oauth2_application": "Cuir Feidhmchlár OAuth2 in eagar", - "settings.oauth2_applications_desc": "Cumasaíonn feidhmchláir OAuth2 d’fheidhmchlár tríú páirtí úsáideoirí a fhíordheimhniú go slán ag an ásc Gitea seo.", + "settings.oauth2_applications_desc": "Cuireann feidhmchláir OAuth2 ar chumas d’fheidhmchlár tríú páirtí úsáideoirí a fhíordheimhniú go slán ag an gcás Gitea seo.", "settings.remove_oauth2_application": "Bain Feidhmchlár OAuth2", "settings.remove_oauth2_application_desc": "Ag baint feidhmchlár OAuth2, cúlghairfear rochtain ar gach comhartha rochtana sínithe. Lean ar aghaidh?", "settings.remove_oauth2_application_success": "Scriosadh an feidhmchlár.", @@ -890,7 +885,7 @@ "settings.oauth2_regenerate_secret_hint": "Chaill tú do rún?", "settings.oauth2_client_secret_hint": "Ní thaispeánfar an rún arís tar éis duit an leathanach seo a fhágáil nó a athnuachan. Déan cinnte le do thoil gur shábháil tú é.", "settings.oauth2_application_edit": "Cuir in eagar", - "settings.oauth2_application_create_description": "Tugann feidhmchláir OAuth2 rochtain d'iarratas tríú páirtí ar chuntais úsáideora ar an gcás seo.", + "settings.oauth2_application_create_description": "Tugann feidhmchláir OAuth2 rochtain do d’fheidhmchlár tríú páirtí ar chuntais úsáideora ar an gcás seo.", "settings.oauth2_application_remove_description": "Cuirfear feidhmchlár OAuth2 a bhaint cosc air rochtain a fháil ar chuntais úsáideora údaraithe ar an gcás seo. Lean ar aghaidh?", "settings.oauth2_application_locked": "Réamhchláraíonn Gitea roinnt feidhmchlár OAuth2 ar thosú má tá sé cumasaithe i gcumraíocht. Chun iompar gan choinne a chosc, ní féidir iad seo a chur in eagar ná a bhaint. Féach do thoil do dhoiciméadú OAuth2 le haghaidh tuilleadh faisnéise.", "settings.authorized_oauth2_applications": "Feidhmchláir Údaraithe OAuth2", @@ -1524,6 +1519,7 @@ "repo.issues.commented_at": "trácht %s ", "repo.issues.delete_comment_confirm": "An bhfuil tú cinnte gur mhaith leat an trácht seo a scriosadh?", "repo.issues.context.copy_link": "Cóipeáil Nasc", + "repo.issues.context.copy_source": "Cóipeáil Foinse", "repo.issues.context.quote_reply": "Luaigh Freagra", "repo.issues.context.reference_issue": "Tagairt in Eagrán Nua", "repo.issues.context.edit": "Cuir in eagar", @@ -3192,7 +3188,6 @@ "admin.config.custom_conf": "Cosán Comhad Cumraíochta", "admin.config.custom_file_root_path": "Cosán Fréamh Comhad Saincheaptha", "admin.config.domain": "Fearann ​​Freastalaí", - "admin.config.offline_mode": "Mód Áitiúil", "admin.config.disable_router_log": "Díchumasaigh Loga an Ródaire", "admin.config.run_user": "Rith Mar Ainm úsáideora", "admin.config.run_mode": "Mód Rith", @@ -3278,6 +3273,13 @@ "admin.config.cache_test_failed": "Theip ar an taisce a thaiscéaladh: %v.", "admin.config.cache_test_slow": "D'éirigh leis an tástáil taisce, ach tá an freagra mall: %s.", "admin.config.cache_test_succeeded": "D'éirigh leis an tástáil taisce, fuair sé freagra i %s.", + "admin.config.common.start_time": "Am tosaithe", + "admin.config.common.end_time": "Am deiridh", + "admin.config.common.skip_time_check": "Fág an t-am folamh (glan an réimse) chun seiceáil ama a scipeáil", + "admin.config.instance_maintenance": "Cothabháil Cásanna", + "admin.config.instance_maintenance_mode.admin_web_access_only": "Lig don riarthóir amháin rochtain a fháil ar chomhéadan gréasáin", + "admin.config.instance_web_banner.enabled": "Taispeáin meirge", + "admin.config.instance_web_banner.message_placeholder": "Teachtaireacht meirge (tacaíonn sé le Markdown)", "admin.config.session_config": "Cumraíocht Seisiúin", "admin.config.session_provider": "Soláthraí Seisiúin", "admin.config.provider_config": "Cumraíocht Soláthraí", @@ -3288,7 +3290,7 @@ "admin.config.cookie_life_time": "Am Saoil Fianán", "admin.config.picture_config": "Cumraíocht Pictiúr agus Avatar", "admin.config.picture_service": "Seirbhís Pictiúr", - "admin.config.disable_gravatar": "Díchumasaigh Gravatar", + "admin.config.enable_gravatar": "Cumasaigh Gravatar", "admin.config.enable_federated_avatar": "Cumasaigh Avatars Cónaidhme", "admin.config.open_with_editor_app_help": "Na heagarthóirí \"Oscailte le\" don roghchlár Clón. Má fhágtar folamh é, úsáidfear an réamhshocrú. Leathnaigh chun an réamhshocrú a fheiceáil.", "admin.config.git_guide_remote_name": "Ainm iargúlta stórais le haghaidh orduithe git sa treoir", @@ -3672,6 +3674,8 @@ "actions.runners.reset_registration_token_confirm": "Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint?", "actions.runners.reset_registration_token_success": "D'éirigh le hathshocrú comhartha clárúcháin an dara háit", "actions.runs.all_workflows": "Gach Sreafaí Oibre", + "actions.runs.workflow_run_count_1": "%d rith sreabha oibre", + "actions.runs.workflow_run_count_n": "%d rith sreabha oibre", "actions.runs.commit": "Tiomantas", "actions.runs.scheduled": "Sceidealaithe", "actions.runs.pushed_by": "bhrú ag", From 315b947740a2e3a5ae8d4cbf594042e473c00734 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 3 Mar 2026 23:15:33 -0800 Subject: [PATCH 03/30] Harden render iframe open-link handling (#36811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR hardens the handling of the “open-link” action in render iframes (external rendering iframes). It prevents iframes from triggering unsafe or unintended redirects or opening new windows via postMessage. Additionally, it improves iframe height reporting to reduce scrollbar and height mismatch issues, and adds unit test coverage. --------- Co-authored-by: wxiaoguang --- web_src/js/markup/render-iframe.test.ts | 46 +++++++++++++++++++ web_src/js/markup/render-iframe.ts | 39 ++++++++++++---- .../js/standalone/external-render-iframe.ts | 20 ++++++-- 3 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 web_src/js/markup/render-iframe.test.ts diff --git a/web_src/js/markup/render-iframe.test.ts b/web_src/js/markup/render-iframe.test.ts new file mode 100644 index 0000000000..53c9dc3720 --- /dev/null +++ b/web_src/js/markup/render-iframe.test.ts @@ -0,0 +1,46 @@ +import {navigateToIframeLink} from './render-iframe.ts'; + +describe('navigateToIframeLink', () => { + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + const assignSpy = vi.spyOn(window.location, 'assign').mockImplementation(() => undefined); + + test('safe links', () => { + navigateToIframeLink('http://example.com', '_blank'); + expect(openSpy).toHaveBeenCalledWith('http://example.com/', '_blank', 'noopener,noreferrer'); + vi.clearAllMocks(); + + navigateToIframeLink('https://example.com', '_self'); + expect(assignSpy).toHaveBeenCalledWith('https://example.com/'); + vi.clearAllMocks(); + + navigateToIframeLink('https://example.com', null); + expect(assignSpy).toHaveBeenCalledWith('https://example.com/'); + vi.clearAllMocks(); + + navigateToIframeLink('/path', ''); + expect(assignSpy).toHaveBeenCalledWith('http://localhost:3000/path'); + vi.clearAllMocks(); + + // input can be any type & any value, keep the same behavior as `window.location.href = 0` + navigateToIframeLink(0, {}); + expect(assignSpy).toHaveBeenCalledWith('http://localhost:3000/0'); + vi.clearAllMocks(); + }); + + test('unsafe links', () => { + window.location.href = 'http://localhost:3000/'; + + // eslint-disable-next-line no-script-url + navigateToIframeLink('javascript:void(0);', '_blank'); + expect(openSpy).toHaveBeenCalledTimes(0); + expect(assignSpy).toHaveBeenCalledTimes(0); + expect(window.location.href).toBe('http://localhost:3000/'); + vi.clearAllMocks(); + + navigateToIframeLink('data:image/svg+xml;utf8,', ''); + expect(openSpy).toHaveBeenCalledTimes(0); + expect(assignSpy).toHaveBeenCalledTimes(0); + expect(window.location.href).toBe('http://localhost:3000/'); + vi.clearAllMocks(); + }); +}); diff --git a/web_src/js/markup/render-iframe.ts b/web_src/js/markup/render-iframe.ts index 1291dea4f8..531942e0b1 100644 --- a/web_src/js/markup/render-iframe.ts +++ b/web_src/js/markup/render-iframe.ts @@ -1,23 +1,46 @@ import {generateElemId, queryElemChildren} from '../utils/dom.ts'; import {isDarkTheme} from '../utils.ts'; +function safeRenderIframeLink(link: any): string | null { + try { + const url = new URL(`${link}`, window.location.href); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + console.error(`Unsupported link protocol: ${link}`); + return null; + } + return url.href; + } catch (e) { + console.error(`Failed to parse link: ${link}, error: ${e}`); + return null; + } +} + +// This function is only designed for "open-link" command from iframe, is not suitable for other contexts. +// Because other link protocols are directly handled by the iframe, but not here. +// Arguments can be any type & any value, they are from "message" event's data which is not controlled by us. +export function navigateToIframeLink(unsafeLink: any, target: any) { + const linkHref = safeRenderIframeLink(unsafeLink); + if (linkHref === null) return; + if (target === '_blank') { + window.open(linkHref, '_blank', 'noopener,noreferrer'); + return; + } + // treat all other targets including ("_top", "_self", etc.) as same tab navigation + window.location.assign(linkHref); +} + async function loadRenderIframeContent(iframe: HTMLIFrameElement) { const iframeSrcUrl = iframe.getAttribute('data-src')!; if (!iframe.id) iframe.id = generateElemId('gitea-iframe-'); window.addEventListener('message', (e) => { + if (e.source !== iframe.contentWindow) return; if (!e.data?.giteaIframeCmd || e.data?.giteaIframeId !== iframe.id) return; const cmd = e.data.giteaIframeCmd; if (cmd === 'resize') { - // TODO: sometimes the reported iframeHeight is not the size we need, need to figure why. Example: openapi swagger. - // As a workaround, add some pixels here. - iframe.style.height = `${e.data.iframeHeight + 2}px`; + iframe.style.height = `${e.data.iframeHeight}px`; } else if (cmd === 'open-link') { - if (e.data.anchorTarget === '_blank') { - window.open(e.data.openLink, '_blank'); - } else { - window.location.href = e.data.openLink; - } + navigateToIframeLink(e.data.openLink, e.data.anchorTarget); } else { throw new Error(`Unknown gitea iframe cmd: ${cmd}`); } diff --git a/web_src/js/standalone/external-render-iframe.ts b/web_src/js/standalone/external-render-iframe.ts index dcfeb50541..f8ec070785 100644 --- a/web_src/js/standalone/external-render-iframe.ts +++ b/web_src/js/standalone/external-render-iframe.ts @@ -20,7 +20,15 @@ function mainExternalRenderIframe() { window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*'); }; - const updateIframeHeight = () => postIframeMsg('resize', {iframeHeight: document.documentElement.scrollHeight}); + const updateIframeHeight = () => { + // Don't use integer heights from the DOM node. + // Use getBoundingClientRect(), then ceil the height to avoid fractional pixels which causes incorrect scrollbars. + const rect = document.documentElement.getBoundingClientRect(); + postIframeMsg('resize', {iframeHeight: Math.ceil(rect.height)}); + // As long as the parent page is responsible for the iframe height, the iframe itself doesn't need scrollbars. + // This style should only be dynamically set here when our code can run. + document.documentElement.style.overflowY = 'hidden'; + }; const resizeObserver = new ResizeObserver(() => updateIframeHeight()); resizeObserver.observe(window.document.documentElement); @@ -29,16 +37,18 @@ function mainExternalRenderIframe() { // the easiest way to handle dynamic content changes and easy to debug, can be fine-tuned in the future setInterval(updateIframeHeight, 1000); - // no way to open an absolute link with CSP frame-src, it also needs some tricks like "postMessage" or "copy the link to clipboard" - const openIframeLink = (link: string, target: string) => postIframeMsg('open-link', {openLink: link, anchorTarget: target}); + // no way to open an absolute link with CSP frame-src, it needs some tricks like "postMessage" (let parent window to handle) or "copy the link to clipboard" (let users manually paste it to open). + // here we choose "postMessage" way for better user experience. + const openIframeLink = (link: string, target: string | null) => postIframeMsg('open-link', {openLink: link, anchorTarget: target}); document.addEventListener('click', (e) => { const el = e.target as HTMLAnchorElement; if (el.nodeName !== 'A') return; - const href = el.getAttribute('href') || ''; + const href = el.getAttribute('href') ?? ''; // safe links: "./any", "../any", "/any", "//host/any", "http://host/any", "https://host/any" if (href.startsWith('.') || href.startsWith('/') || href.startsWith('http://') || href.startsWith('https://')) { e.preventDefault(); - openIframeLink(href, el.getAttribute('target')!); + const forceTarget = (e.metaKey || e.ctrlKey) ? '_blank' : null; + openIframeLink(href, forceTarget ?? el.getAttribute('target')); } }); } From 79ae9ea97b8d0c6267901c62c9ceae830cd36089 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 4 Mar 2026 21:23:17 +0800 Subject: [PATCH 04/30] fix(repo): unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597) The design of DefaultShowFullName has some problems, which make the UI inconsistent, see the new comment in code This PR does a clean up for various legacy problems, and clarify some "user name display" behaviors. --------- Co-authored-by: wxiaoguang --- models/repo/user_repo.go | 16 +++-- models/repo/user_repo_test.go | 4 +- models/user/user.go | 64 ++++++++++--------- modules/setting/ui.go | 10 ++- modules/templates/helper.go | 3 - modules/templates/util_avatar.go | 9 +-- routers/web/repo/issue_content_history.go | 29 ++++----- routers/web/repo/issue_poster.go | 7 +- services/mailer/mail.go | 13 ++-- templates/admin/org/list.tmpl | 7 +- templates/repo/commits_list.tmpl | 21 +++--- templates/repo/graph/commits.tmpl | 11 ++-- .../repo/issue/filter_item_user_assign.tmpl | 2 +- templates/repo/latest_commit.tmpl | 22 +++---- templates/repo/search_name.tmpl | 2 +- tests/integration/repo_commits_test.go | 11 ++-- web_src/css/repo.css | 10 ++- web_src/js/features/repo-issue-list.ts | 9 +-- 18 files changed, 128 insertions(+), 122 deletions(-) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 08cf964bc8..e15a64b01e 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -147,19 +147,21 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us } // GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository -// If isShowFullName is set to true, also include full name prefix search -func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { +// It searches with the "user.name" and "user.full_name" fields case-insensitively. +func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) { users := make([]*user_model.User, 0, 30) - var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} - if search != "" && isShowFullName { - prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) - } cond := builder.In("`user`.id", builder.Select("poster_id").From("issue").Where( builder.Eq{"repo_id": repo.ID}. And(builder.Eq{"is_pull": isPull}), - ).GroupBy("poster_id")).And(prefixCond) + ).GroupBy("poster_id")) + + if search != "" { + var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} + prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) + cond = cond.And(prefixCond) + } return users, db.GetEngine(ctx). Where(cond). diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index a53cf39dc4..cd8a0f1a1f 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -44,12 +44,12 @@ func TestGetIssuePostersWithSearch(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER", false /* full name */) + users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER") require.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "user2", users[0].Name) - users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O", true /* full name */) + users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O") require.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "user2", users[0].Name) diff --git a/models/user/user.go b/models/user/user.go index d8f41b869e..a74662bb12 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -8,6 +8,7 @@ import ( "context" "encoding/hex" "fmt" + "html/template" "mime" "net/mail" "net/url" @@ -28,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -417,16 +419,6 @@ func (u *User) IsTokenAccessAllowed() bool { return u.Type == UserTypeIndividual || u.Type == UserTypeBot } -// DisplayName returns full name if it's not empty, -// returns username otherwise. -func (u *User) DisplayName() string { - trimmed := strings.TrimSpace(u.FullName) - if len(trimmed) > 0 { - return trimmed - } - return u.Name -} - // EmailTo returns a string suitable to be put into a e-mail `To:` header. func (u *User) EmailTo() string { sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName()) @@ -445,27 +437,45 @@ func (u *User) EmailTo() string { return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address) } -// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, -// returns username otherwise. +// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them +// * user.Name / user.FullName: directly used in templates +// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name +// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name +// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles +// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy + +// DisplayName returns full name if it's not empty, returns username otherwise. +func (u *User) DisplayName() string { + fullName := strings.TrimSpace(u.FullName) + if fullName != "" { + return fullName + } + return u.Name +} + +// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username. func (u *User) GetDisplayName() string { if setting.UI.DefaultShowFullName { - trimmed := strings.TrimSpace(u.FullName) - if len(trimmed) > 0 { - return trimmed + fullName := strings.TrimSpace(u.FullName) + if fullName != "" { + return fullName } } return u.Name } -// GetCompleteName returns the full name and username in the form of -// "Full Name (username)" if full name is not empty, otherwise it returns -// "username". -func (u *User) GetCompleteName() string { - trimmedFullName := strings.TrimSpace(u.FullName) - if len(trimmedFullName) > 0 { - return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name) +// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME +func (u *User) ShortName(length int) string { + return util.EllipsisDisplayString(u.GetDisplayName(), length) +} + +func (u *User) GetShortDisplayNameLinkHTML() template.HTML { + fullName := strings.TrimSpace(u.FullName) + displayName, displayTooltip := u.Name, fullName + if setting.UI.DefaultShowFullName && fullName != "" { + displayName, displayTooltip = fullName, u.Name } - return u.Name + return htmlutil.HTMLFormat(`%s`, u.HomeLink(), displayTooltip, displayName) } func gitSafeName(name string) string { @@ -488,14 +498,6 @@ func (u *User) GitName() string { return fmt.Sprintf("user-%d", u.ID) } -// ShortName ellipses username to length -func (u *User) ShortName(length int) string { - if setting.UI.DefaultShowFullName && len(u.FullName) > 0 { - return util.EllipsisDisplayString(u.FullName, length) - } - return util.EllipsisDisplayString(u.Name, length) -} - // IsMailable checks if a user is eligible to receive emails. // System users like Ghost and Gitea Actions are excluded. func (u *User) IsMailable() bool { diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 77a5b45d0a..722341a71e 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -25,7 +25,6 @@ var UI = struct { ReactionMaxUserNum int MaxDisplayFileSize int64 ShowUserEmail bool - DefaultShowFullName bool DefaultTheme string Themes []string FileIconTheme string @@ -43,6 +42,15 @@ var UI = struct { AmbiguousUnicodeDetection bool + // TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710 + // But there are still many edge cases: + // * Many places still use "username", not respecting this setting + // * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't + // * Even if DefaultShowFullName=false, many places still need to show the full name + // For most cases, either "username" or "username (Full Name)" should be used and are good enough. + // Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used. + DefaultShowFullName bool + Notification struct { MinTimeout time.Duration TimeoutStep time.Duration diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 11c52bd5a7..82087568df 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -96,9 +96,6 @@ func NewFuncMap() template.FuncMap { "AssetVersion": func() string { return setting.AssetVersion }, - "DefaultShowFullName": func() bool { - return setting.UI.DefaultShowFullName - }, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index ee9994ab0b..524c64d0b6 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -16,6 +16,7 @@ import ( user_model "code.gitea.io/gitea/models/user" gitea_html "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) type AvatarUtils struct { @@ -29,13 +30,9 @@ func NewAvatarUtils(ctx context.Context) *AvatarUtils { // AvatarHTML creates the HTML for an avatar func AvatarHTML(src string, size int, class, name string) template.HTML { sizeStr := strconv.Itoa(size) - - if name == "" { - name = "avatar" - } - + name = util.IfZero(name, "avatar") // use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width - return template.HTML(``) + return template.HTML(``) } // Avatar renders user avatars. args: user, size (int), class (string) diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index a56df78163..23cedfcb80 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -6,13 +6,14 @@ package repo import ( "bytes" "html" + "html/template" "net/http" "strings" "code.gitea.io/gitea/models/avatars" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" @@ -53,29 +54,25 @@ func GetContentHistoryList(ctx *context.Context) { // value is historyId var results []map[string]any for _, item := range items { - var actionText string + var actionHTML template.HTML if item.IsDeleted { - actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted") - actionText = "" + actionTextDeleted + "" + actionHTML = htmlutil.HTMLFormat(`%s`, ctx.Locale.TrString("repo.issues.content_history.deleted")) } else if item.IsFirstCreated { - actionText = ctx.Locale.TrString("repo.issues.content_history.created") + actionHTML = ctx.Locale.Tr("repo.issues.content_history.created") } else { - actionText = ctx.Locale.TrString("repo.issues.content_history.edited") + actionHTML = ctx.Locale.Tr("repo.issues.content_history.edited") } - username := item.UserName - if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" { - username = strings.TrimSpace(item.UserFullName) + var fullNameHTML template.HTML + userName, fullName := item.UserName, strings.TrimSpace(item.UserFullName) + if fullName != "" { + fullNameHTML = htmlutil.HTMLFormat(` (%s)`, fullName) } - src := html.EscapeString(item.UserAvatarLink) - class := avatars.DefaultAvatarClass + " tw-mr-2" - name := html.EscapeString(username) - avatarHTML := string(templates.AvatarHTML(src, 28, class, username)) - timeSinceHTML := string(templates.TimeSince(item.EditedUnix)) - + avatarHTML := templates.AvatarHTML(item.UserAvatarLink, 24, avatars.DefaultAvatarClass+" tw-mr-2", userName) + timeSinceHTML := templates.TimeSince(item.EditedUnix) results = append(results, map[string]any{ - "name": avatarHTML + "" + name + " " + actionText + " " + timeSinceHTML, + "name": htmlutil.HTMLFormat("%s %s%s %s %s", avatarHTML, userName, fullNameHTML, actionHTML, timeSinceHTML), "value": item.HistoryID, }) } diff --git a/routers/web/repo/issue_poster.go b/routers/web/repo/issue_poster.go index 07059b9b7b..4f00f40a91 100644 --- a/routers/web/repo/issue_poster.go +++ b/routers/web/repo/issue_poster.go @@ -10,7 +10,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -34,7 +33,7 @@ func IssuePullPosters(ctx *context.Context) { func issuePosters(ctx *context.Context, isPullList bool) { repo := ctx.Repo.Repository search := strings.TrimSpace(ctx.FormString("q")) - posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName) + posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search) if err != nil { ctx.JSON(http.StatusInternalServerError, err) return @@ -54,9 +53,7 @@ func issuePosters(ctx *context.Context, isPullList bool) { resp.Results = make([]*userSearchInfo, len(posters)) for i, user := range posters { resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)} - if setting.UI.DefaultShowFullName { - resp.Results[i].FullName = user.FullName - } + resp.Results[i].FullName = user.FullName } ctx.JSON(http.StatusOK, resp) } diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 8f831f89ad..a08ed71480 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -158,18 +158,23 @@ func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ct func fromDisplayName(u *user_model.User) string { if setting.MailService.FromDisplayNameFormatTemplate != nil { - var ctx bytes.Buffer - err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{ + var buf bytes.Buffer + err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{ "DisplayName": u.DisplayName(), "AppName": setting.AppName, "Domain": setting.Domain, }) if err == nil { - return mime.QEncoding.Encode("utf-8", ctx.String()) + return mime.QEncoding.Encode("utf-8", buf.String()) } log.Error("fromDisplayName: %w", err) } - return u.GetCompleteName() + def := u.Name + if fullName := strings.TrimSpace(u.FullName); fullName != "" { + // use "Full Name (username)" for email's sender name if Full Name is not empty + def = fullName + " (" + u.Name + ")" + } + return def } func generateMetadataHeaders(repo *repo_model.Repository) map[string]string { diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index a4317b8c4e..e6de93c5f8 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -48,11 +48,14 @@ - {{range .Users}} + {{range $org := .Users}} {{.ID}} - {{if and DefaultShowFullName .FullName}}{{.FullName}} ({{.Name}}){{else}}{{.Name}}{{end}} + + {{$org.Name}} + {{if $org.FullName}}({{$org.FullName}}){{end}} + {{if .Visibility.IsPrivate}} {{svg "octicon-lock"}} {{end}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 1a236582a2..a0722307a7 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -14,18 +14,15 @@ {{range .Commits}} -
- {{$userName := .Author.Name}} - {{if .User}} - {{if and .User.FullName DefaultShowFullName}} - {{$userName = .User.FullName}} - {{end}} - {{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{$userName}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}} - {{$userName}} - {{end}} -
+ + {{- if .User -}} + {{- ctx.AvatarUtils.Avatar .User 20 "tw-mr-2" -}} + {{- .User.GetShortDisplayNameLinkHTML -}} + {{- else -}} + {{- ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20 "tw-mr-2" -}} + {{- .Author.Name -}} + {{- end -}} + {{$commitBaseLink := ""}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index d92be9c5ed..d86f73fe65 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -41,16 +41,13 @@ - {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} - {{if and $commit.User.FullName DefaultShowFullName}} - {{$userName = $commit.User.FullName}} - {{end}} {{ctx.AvatarUtils.Avatar $commit.User 18}} - {{$userName}} + {{$commit.User.GetShortDisplayNameLinkHTML}} {{else}} - {{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName 18}} - {{$userName}} + {{$gitUserName := $commit.Commit.Author.Name}} + {{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $gitUserName 18}} + {{$gitUserName}} {{end}} diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl index 42886edaa0..5ca8a8079c 100644 --- a/templates/repo/issue/filter_item_user_assign.tmpl +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -10,7 +10,7 @@ {{$queryLink := .QueryLink}}