Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions modules/setting/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)

// SessionConfig defines Session settings
Expand Down Expand Up @@ -49,10 +50,8 @@ func loadSessionFrom(rootCfg ConfigProvider) {
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
}
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
SessionConfig.CookiePath = AppSubURL
if SessionConfig.CookiePath == "" {
SessionConfig.CookiePath = "/"
}
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
Expand Down
3 changes: 3 additions & 0 deletions modules/svg/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ func MockIcon(icon string) func() {

// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
func RenderHTML(icon string, others ...any) template.HTML {
if icon == "" {
return ""
}
size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
if svgStr, ok := svgIcons[icon]; ok {
// the code is somewhat hacky, but it just works, because the SVG contents are all normalized
Expand Down
13 changes: 0 additions & 13 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"strings"
"time"

user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup"
Expand All @@ -21,7 +20,6 @@ import (
"code.gitea.io/gitea/modules/templates/eval"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
"code.gitea.io/gitea/services/webtheme"
)

// NewFuncMap returns functions for injecting to templates
Expand Down Expand Up @@ -130,7 +128,6 @@ func NewFuncMap() template.FuncMap {
"DisableWebhooks": func() bool {
return setting.DisableWebhooks
},
"UserThemeName": userThemeName,
"NotificationSettings": func() map[string]any {
return map[string]any{
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
Expand Down Expand Up @@ -217,16 +214,6 @@ func evalTokens(tokens ...any) (any, error) {
return n.Value, err
}

func userThemeName(user *user_model.User) string {
if user == nil || user.Theme == "" {
return setting.UI.DefaultTheme
}
if webtheme.IsThemeAvailable(user.Theme) {
return user.Theme
}
return setting.UI.DefaultTheme
}

func isQueryParamEmpty(v any) bool {
return v == nil || v == false || v == 0 || v == int64(0) || v == ""
}
Expand Down
17 changes: 17 additions & 0 deletions modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import (
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/webtheme"
)

type RenderUtils struct {
Expand Down Expand Up @@ -259,3 +261,18 @@ func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink strin
htmlCode += "</span>"
return template.HTML(htmlCode)
}

func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {
svgName := "octicon-paintbrush"
switch info.ColorScheme {
case "dark":
svgName = "octicon-moon"
case "light":
svgName = "octicon-sun"
case "auto":
svgName = "gitea-eclipse"
}
icon := svg.RenderHTML(svgName, iconSize)
extraIcon := svg.RenderHTML(info.GetExtraIconName(), iconSize)
return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, info.GetDescription(), icon, info.DisplayName, extraIcon)
}
5 changes: 4 additions & 1 deletion modules/web/middleware/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)

// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently
Expand Down Expand Up @@ -39,11 +40,13 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
// These are more specific than cookies without a trailing /, so
// we need to delete these if they exist.
deleteLegacySiteCookie(resp, name)

// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
cookie := &http.Cookie{
Name: name,
Value: url.QueryEscape(value),
MaxAge: maxAge,
Path: setting.SessionConfig.CookiePath,
Path: util.IfZero(setting.SessionConfig.CookiePath, "/"),
Domain: setting.SessionConfig.Domain,
Secure: setting.SessionConfig.Secure,
HttpOnly: true,
Expand Down
1 change: 1 addition & 0 deletions public/assets/img/svg/gitea-colorblind-redgreen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/assets/img/svg/gitea-eclipse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion routers/common/errpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

tmplCtx := context.TemplateContext{}
tmplCtx := context.NewTemplateContext(req.Context(), req)
tmplCtx["Locale"] = middleware.Locale(w, req)
ctxData := middleware.GetContextData(req.Context())

Expand Down
2 changes: 1 addition & 1 deletion routers/common/qos.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {
return
}

tmplCtx := giteacontext.TemplateContext{}
tmplCtx := giteacontext.NewTemplateContext(req.Context(), req)
tmplCtx["Locale"] = middleware.Locale(w, req)
ctxData := middleware.GetContextData(req.Context())
err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)
Expand Down
5 changes: 5 additions & 0 deletions routers/install/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/routers/web/misc"
"code.gitea.io/gitea/services/forms"
)

Expand All @@ -32,7 +33,11 @@ func Routes() *web.Router {
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone)

r.Get("/-/web-theme/list", misc.WebThemeList)
r.Post("/-/web-theme/apply", misc.WebThemeApply)
r.Get("/api/healthz", healthcheck.Check)

r.NotFound(installNotFound)

base.Mount("", r)
Expand Down
42 changes: 42 additions & 0 deletions routers/web/misc/webtheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package misc

import (
"net/http"

"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
"code.gitea.io/gitea/services/webtheme"
)

func WebThemeList(ctx *context.Context) {
curWebTheme := ctx.TemplateContext.CurrentWebTheme()
renderUtils := templates.NewRenderUtils(ctx)
allThemes := webtheme.GetAvailableThemes()

var results []map[string]any
for _, theme := range allThemes {
results = append(results, map[string]any{
"name": renderUtils.RenderThemeItem(theme, 14),
"value": theme.InternalName,
"class": "item js-aria-clickable" + util.Iif(theme.InternalName == curWebTheme.InternalName, " selected", ""),
})
}
ctx.JSON(http.StatusOK, map[string]any{"results": results})
}

func WebThemeApply(ctx *context.Context) {
themeName := ctx.FormString("theme")
if ctx.Doer != nil {
opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)}
_ = user_service.UpdateUser(ctx, ctx.Doer, opts)
} else {
middleware.SetSiteCookie(ctx.Resp, "gitea_theme", themeName, 0)
}
}
2 changes: 1 addition & 1 deletion routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func UpdateUIThemePost(ctx *context.Context) {
return
}

if !webtheme.IsThemeAvailable(form.Theme) {
if webtheme.GetThemeMetaInfo(form.Theme) == nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
Expand Down
3 changes: 3 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ func registerWebRoutes(m *web.Router) {

m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)

m.Get("/-/web-theme/list", misc.WebThemeList)
m.Post("/-/web-theme/apply", optSignInIgnoreCsrf, misc.WebThemeApply)

m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/explore/repos")
Expand Down
2 changes: 1 addition & 1 deletion services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
}

func NewTemplateContextForWeb(ctx *Context) TemplateContext {
tmplCtx := NewTemplateContext(ctx)
tmplCtx := NewTemplateContext(ctx, ctx.Req)
tmplCtx["Locale"] = ctx.Base.Locale
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx)
Expand Down
23 changes: 21 additions & 2 deletions services/context/context_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ package context

import (
"context"
"net/http"
"time"

"code.gitea.io/gitea/services/webtheme"
)

var _ context.Context = TemplateContext(nil)

func NewTemplateContext(ctx context.Context) TemplateContext {
return TemplateContext{"_ctx": ctx}
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
return TemplateContext{"_ctx": ctx, "_req": req}
}

func (c TemplateContext) parentContext() context.Context {
Expand All @@ -33,3 +36,19 @@ func (c TemplateContext) Err() error {
func (c TemplateContext) Value(key any) any {
return c.parentContext().Value(key)
}

func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo {
req := c["_req"].(*http.Request)
var themeName string
if webCtx := GetWebContext(c); webCtx != nil {
if webCtx.Doer != nil {
themeName = webCtx.Doer.Theme
}
}
if themeName == "" {
if cookieTheme, _ := req.Cookie("gitea_theme"); cookieTheme != nil {
themeName = cookieTheme.Value
}
}
return webtheme.GuaranteeGetThemeMetaInfo(themeName)
}
59 changes: 47 additions & 12 deletions services/webtheme/webtheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import (
)

var (
availableThemes []*ThemeMetaInfo
availableThemeInternalNames container.Set[string]
themeOnce sync.Once
availableThemes []*ThemeMetaInfo
availableThemeMap map[string]*ThemeMetaInfo
themeOnce sync.Once
)

const (
Expand All @@ -28,9 +28,25 @@ const (
)

type ThemeMetaInfo struct {
FileName string
InternalName string
DisplayName string
FileName string
InternalName string
DisplayName string
ColorblindType string
ColorScheme string
}

func (info *ThemeMetaInfo) GetDescription() string {
if info.ColorblindType == "red-green" {
return "Red-green colorblind friendly"
}
return ""
}

func (info *ThemeMetaInfo) GetExtraIconName() string {
if info.ColorblindType == "red-green" {
return "gitea-colorblind-redgreen"
}
return ""
}

func parseThemeMetaInfoToMap(cssContent string) map[string]string {
Expand All @@ -54,7 +70,7 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string {
|('(\\'|[^'])*')
|([^'";]+)
)
\s*;
\s*;?
\s*
)
`
Expand Down Expand Up @@ -102,17 +118,19 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
return themeInfo
}
themeInfo.DisplayName = m["--theme-display-name"]
themeInfo.ColorblindType = m["--theme-colorblind-type"]
themeInfo.ColorScheme = m["--theme-color-scheme"]
return themeInfo
}

func initThemes() {
availableThemes = nil
defer func() {
availableThemeInternalNames = container.Set[string]{}
availableThemeMap = map[string]*ThemeMetaInfo{}
for _, theme := range availableThemes {
availableThemeInternalNames.Add(theme.InternalName)
availableThemeMap[theme.InternalName] = theme
}
if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
if availableThemeMap[setting.UI.DefaultTheme] == nil {
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
}
}()
Expand Down Expand Up @@ -147,6 +165,9 @@ func initThemes() {
if availableThemes[i].InternalName == setting.UI.DefaultTheme {
return true
}
if availableThemes[i].ColorblindType != availableThemes[j].ColorblindType {
return availableThemes[i].ColorblindType < availableThemes[j].ColorblindType
}
return availableThemes[i].DisplayName < availableThemes[j].DisplayName
})
if len(availableThemes) == 0 {
Expand All @@ -160,7 +181,21 @@ func GetAvailableThemes() []*ThemeMetaInfo {
return availableThemes
}

func IsThemeAvailable(internalName string) bool {
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
themeOnce.Do(initThemes)
return availableThemeInternalNames.Contains(internalName)
return availableThemeMap[internalName]
}

// GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo,
// to simplify the caller's logic, especially for templates.
// There are already enough warnings messages if the default theme is not available.
func GuaranteeGetThemeMetaInfo(internalName string) *ThemeMetaInfo {
info := GetThemeMetaInfo(internalName)
if info == nil {
info = GetThemeMetaInfo(setting.UI.DefaultTheme)
}
if info == nil {
info = &ThemeMetaInfo{DisplayName: "unavailable", InternalName: "unavailable", FileName: "unavailable"}
}
return info
}
6 changes: 6 additions & 0 deletions services/webtheme/webtheme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ gitea-theme-meta-info {
--k2: real;
}`)
assert.Equal(t, map[string]string{"--k2": "real"}, m)

// compressed CSS, no trailing semicolon
m = parseThemeMetaInfoToMap(`gitea-theme-meta-info{--k1:"v1"}`)
assert.Equal(t, map[string]string{"--k1": "v1"}, m)
m = parseThemeMetaInfoToMap(`gitea-theme-meta-info{--k1:"v1";--k2:"v2"}`)
assert.Equal(t, map[string]string{"--k1": "v1", "--k2": "v2"}, m)
}
Loading