Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
41 changes: 41 additions & 0 deletions routers/web/misc/webtheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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")
middleware.SetSiteCookie(ctx.Resp, "gitea_theme", themeName, 0)
if ctx.Doer != nil {
opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)}
_ = user_service.UpdateUser(ctx, ctx.Doer, opts)
}
}
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 @@ -497,6 +497,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