From 0f23219ee4083e64a565ac5b124239d4cd37090b Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 25 May 2026 23:41:47 -0700 Subject: [PATCH 1/5] fix: http content file render (#37850) (#37856) Backport #37850 Fix #37849 Signed-off-by: wxiaoguang Co-authored-by: wxiaoguang Co-authored-by: TheFox0x7 --- modules/highlight/highlight_test.go | 16 ++++++++++++++-- modules/highlight/lexerdetect.go | 8 ++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index 211132b255..a31f8752f1 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -63,7 +63,7 @@ func TestFile(t *testing.T) { { name: "tags.py", code: "<>", - want: lines(`<>`), + want: lines(`<>`), lexerName: "Python", }, { @@ -102,7 +102,7 @@ c=2 def:\n a=1\n \n -b=''\n +b=''\n \n c=2`, ), @@ -114,6 +114,18 @@ c=2 want: []template.HTML{"--\n", `SELECT`}, lexerName: "SQL", }, + { + name: "test.http", + code: `HTTP/1.0 400 Bad request +Content-Type: text/html + +`, + want: lines(`HTTP/1.0 400 Bad request\n +Content-Type: text/html\n +\n +<html></html>`), + lexerName: "HTTP", + }, } for _, tt := range tests { diff --git a/modules/highlight/lexerdetect.go b/modules/highlight/lexerdetect.go index b31080e8a6..ff6ec1a9a4 100644 --- a/modules/highlight/lexerdetect.go +++ b/modules/highlight/lexerdetect.go @@ -288,24 +288,24 @@ func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lex // if lang is provided, and it matches a lexer, use it directly if byLang { - return lexer + return chroma.Coalesce(lexer) } // if a lexer is detected and there is no conflict for the file extension, use it directly fileExt := path.Ext(fileName) _, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt] if !hasConflicts && lexer != lexers.Fallback { - return lexer + return chroma.Coalesce(lexer) } // try to detect language by content, for best guessing for the language // when using "code" to detect, analyze.GetCodeLanguage is slow, it iterates many rules to detect language from content analyzedLanguage := analyze.GetCodeLanguage(fileName, code) - lexer = DetectChromaLexerByFileName(fileName, analyzedLanguage) + lexer, _ = detectChromaLexerByFileName(fileName, analyzedLanguage) if lexer == lexers.Fallback { if analyzedLanguage != enry.OtherLanguage { log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName) } } - return lexer + return chroma.Coalesce(lexer) } -- 2.52.0 From 1032ae4268ad404c02428fcfb74c806f1131093d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 13:11:15 -0500 Subject: [PATCH 2/5] feat: organization-level 2FA requirement for members (#208) Adds a Require2FA toggle to organization settings. When enabled, org members without 2FA are redirected to the security settings page with a warning flash message. Changes: - New Require2FA field on User model (migration v333) - Org settings UI checkbox with shield-lock icon - Check2FARequirement middleware on member-required org routes - UpdateOptions extended with Require2FA field Closes #208 Co-Authored-By: Claude Opus 4.6 (1M context) --- models/migrations/migrations.go | 1 + models/migrations/v1_27/v333.go | 16 ++++++++++++ models/user/user.go | 3 +++ routers/web/org/require2fa.go | 39 +++++++++++++++++++++++++++++ routers/web/org/setting.go | 2 ++ routers/web/web.go | 2 +- services/user/update.go | 6 +++++ templates/org/settings/options.tmpl | 10 ++++++++ 8 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 models/migrations/v1_27/v333.go create mode 100644 routers/web/org/require2fa.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 627bc04dfa..e362332ca5 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -410,6 +410,7 @@ func prepareMigrationTasks() []*migration { newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel), newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable), + newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser), } return preparedMigrations } diff --git a/models/migrations/v1_27/v333.go b/models/migrations/v1_27/v333.go new file mode 100644 index 0000000000..e245b6d73d --- /dev/null +++ b/models/migrations/v1_27/v333.go @@ -0,0 +1,16 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package v1_27 + +import ( + "xorm.io/xorm" +) + +func AddRequire2FAToUser(x *xorm.Engine) error { + type User struct { + Require2FA bool `xorm:"NOT NULL DEFAULT false"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(User)) + return err +} diff --git a/models/user/user.go b/models/user/user.go index 2f259a3416..f7dcc407e8 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -117,6 +117,9 @@ type User struct { // Maximum repository creation limit, -1 means use global default MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` + // Require2FA when true (and user is an org), all org members must have 2FA enabled + Require2FA bool `xorm:"NOT NULL DEFAULT false"` + // IsActive true: primary email is activated, user can access Web UI and Git SSH. // false: an inactive user can only log in Web UI for account operations (ex: activate the account by email), no other access. IsActive bool `xorm:"INDEX"` diff --git a/routers/web/org/require2fa.go b/routers/web/org/require2fa.go new file mode 100644 index 0000000000..445069c59d --- /dev/null +++ b/routers/web/org/require2fa.go @@ -0,0 +1,39 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package org + +import ( + "net/http" + + auth_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" +) + +// Check2FARequirement checks if the current org requires 2FA and if the user has it enabled. +// If the user doesn't have 2FA and the org requires it, redirect to 2FA setup page. +func Check2FARequirement(ctx *context.Context) { + if ctx.Org == nil || ctx.Org.Organization == nil || ctx.Doer == nil { + return + } + + if !ctx.Org.Organization.Require2FA { + return + } + + // Check if user has 2FA enabled + has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("HasTwoFactorOrWebAuthn", err) + return + } + + if has { + return + } + + // User doesn't have 2FA — show warning and redirect to settings + ctx.Flash.Warning("This organization requires two-factor authentication. Please enable 2FA to continue.") + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index e9153cd6c2..9ba6c03840 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -80,12 +80,14 @@ func SettingsPost(ctx *context.Context) { return } + require2FA := ctx.FormBool("require_2fa") opts := &user_service.UpdateOptions{ FullName: optional.FromPtr(form.FullName), Description: optional.FromPtr(form.Description), Website: optional.FromPtr(form.Website), Location: optional.FromPtr(form.Location), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), + Require2FA: optional.Some(require2FA), } if ctx.Doer.IsAdmin { opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation) diff --git a/routers/web/web.go b/routers/web/web.go index 3a831b02c1..452ad77302 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -960,7 +960,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones) m.Post("/members/action/{action}", org.MembersAction) m.Get("/teams", org.Teams) - }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true})) + }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}), org.Check2FARequirement) m.Group("/{org}", func() { m.Get("/teams/{team}", org.TeamMembers) diff --git a/services/user/update.go b/services/user/update.go index 988e6dd2c8..64356a7b9a 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -56,6 +56,7 @@ type UpdateOptions struct { EmailNotificationsPreference optional.Option[string] SetLastLogin bool RepoAdminChangeTeamAccess optional.Option[bool] + Require2FA optional.Option[bool] } func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error { @@ -169,6 +170,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er cols = append(cols, "repo_admin_change_team_access") } + if opts.Require2FA.Has() { + u.Require2FA = opts.Require2FA.Value() + + cols = append(cols, "require_2fa") + } if opts.EmailNotificationsPreference.Has() { u.EmailNotificationsPreference = opts.EmailNotificationsPreference.Value() diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 614e861b09..5bd9bda4e4 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -48,6 +48,16 @@ {{end}} +
+ +
+
+ + +
+

When enabled, organization members without 2FA configured will be prompted to set it up before accessing organization resources.

+
+
-- 2.52.0 From 1fb97eeeeb6969014cb816291e5551a98490ce42 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 13:22:21 -0500 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20smart=20wiki=20filenames=20?= =?UTF-8?q?=E2=80=94=20sanitize=20special=20characters=20to=20hyphens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New wiki page titles are now sanitized before creating the git file: - Spaces and special characters replaced with hyphens - Consecutive hyphens collapsed to single hyphen - Leading/trailing hyphens trimmed Examples: - "My Page Name" -> "My-Page-Name" - "API & Docs (v2)" -> "API-Docs-v2" - "100% Complete!!" -> "100-Complete" Only affects NEW pages. Existing wiki pages with legacy filenames (spaces, URL encoding) continue to work — the read path is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) --- services/wiki/wiki_path.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index 91363322e1..4961cdb75c 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -6,6 +6,7 @@ package wiki import ( "net/url" "path" + "regexp" "strings" repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" @@ -148,10 +149,24 @@ func WebPathFromRequest(s string) WebPath { return WebPath(s) } +var multiHyphenRe = regexp.MustCompile(`-{2,}`) +var nonAlphanumRe = regexp.MustCompile(`[^a-zA-Z0-9\-]`) + +// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug. +// Spaces and special characters become hyphens, consecutive hyphens collapse to one. +func sanitizeWikiTitle(title string) string { + title = strings.TrimSpace(title) + title = strings.ReplaceAll(title, " ", "-") + title = nonAlphanumRe.ReplaceAllString(title, "-") + title = multiHyphenRe.ReplaceAllString(title, "-") + title = strings.Trim(title, "-") + return title +} + func UserTitleToWebPath(base, title string) WebPath { // TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory. // So we do not add the support for writing slashes in title at the moment. - title = strings.TrimSpace(title) + title = sanitizeWikiTitle(title) title = util.PathJoinRelX(base, escapeSegToWeb(title, false)) if title == "" || title == "." { title = "unnamed" -- 2.52.0 From 0cc7297f23cdd88acd89f840e00f1495f64134d9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 13:39:15 -0500 Subject: [PATCH 4/5] fix: remove unused net/http import in require2fa.go Co-Authored-By: Claude Opus 4.6 (1M context) --- routers/web/org/require2fa.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/routers/web/org/require2fa.go b/routers/web/org/require2fa.go index 445069c59d..4fda89425b 100644 --- a/routers/web/org/require2fa.go +++ b/routers/web/org/require2fa.go @@ -4,8 +4,6 @@ package org import ( - "net/http" - auth_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" -- 2.52.0 From d609b8db8c0aa2eafd177ca18bfe8e1d4f34e1db Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 13:47:59 -0500 Subject: [PATCH 5/5] fix: preserve + and . in wiki slugs, clean stray plus signs Allow C++, .NET, version numbers (2.0.1) in wiki filenames. Clean up isolated plus signs that appear between hyphens. Examples: - C++ vs C# -> C++-vs-C.md - .NET Guide -> .NET-Guide.md - version 2.0.1 -> version-2.0.1-release.md Co-Authored-By: Claude Opus 4.6 (1M context) --- services/wiki/wiki_path.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index 4961cdb75c..9054127f44 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -150,16 +150,18 @@ func WebPathFromRequest(s string) WebPath { } var multiHyphenRe = regexp.MustCompile(`-{2,}`) -var nonAlphanumRe = regexp.MustCompile(`[^a-zA-Z0-9\-]`) +var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`) // sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug. // Spaces and special characters become hyphens, consecutive hyphens collapse to one. +// Preserves: letters, digits, hyphens, plus signs (+), and dots (.) func sanitizeWikiTitle(title string) string { title = strings.TrimSpace(title) title = strings.ReplaceAll(title, " ", "-") - title = nonAlphanumRe.ReplaceAllString(title, "-") + title = nonSlugRe.ReplaceAllString(title, "-") title = multiHyphenRe.ReplaceAllString(title, "-") - title = strings.Trim(title, "-") + title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs + title = strings.Trim(title, "-+.") return title } -- 2.52.0