Backport #30890 by wxiaoguang Follow #27011 Follow #30885 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io>tags/v1.22.0
@@ -3320,6 +3320,7 @@ self_check.database_collation_case_insensitive = Database is using a collation % | |||
self_check.database_inconsistent_collation_columns = Database is using collation %s, but these columns are using mismatched collations. It might cause some unexpected problems. | |||
self_check.database_fix_mysql = For MySQL/MariaDB users, you could use the "gitea doctor convert" command to fix the collation problems, or you could also fix the problem by "ALTER ... COLLATE ..." SQLs manually. | |||
self_check.database_fix_mssql = For MSSQL users, you could only fix the problem by "ALTER ... COLLATE ..." SQLs manually at the moment. | |||
self_check.location_origin_mismatch = Current URL (%[1]s) doesn't match the URL seen by Gitea (%[2]s). If you are using a reverse proxy, please make sure the "Host" and "X-Forwarded-Proto" headers are set correctly. | |||
[action] | |||
create_repo = created repository <a href="%s">%s</a> |
@@ -9,12 +9,14 @@ import ( | |||
"net/http" | |||
"runtime" | |||
"sort" | |||
"strings" | |||
"time" | |||
activities_model "code.gitea.io/gitea/models/activities" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/graceful" | |||
"code.gitea.io/gitea/modules/httplib" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
@@ -223,6 +225,16 @@ func SelfCheck(ctx *context.Context) { | |||
ctx.HTML(http.StatusOK, tplSelfCheck) | |||
} | |||
func SelfCheckPost(ctx *context.Context) { | |||
var problems []string | |||
frontendAppURL := ctx.FormString("location_origin") + setting.AppSubURL + "/" | |||
ctxAppURL := httplib.GuessCurrentAppURL(ctx) | |||
if !strings.HasPrefix(ctxAppURL, frontendAppURL) { | |||
problems = append(problems, ctx.Locale.TrString("admin.self_check.location_origin_mismatch", frontendAppURL, ctxAppURL)) | |||
} | |||
ctx.JSON(http.StatusOK, map[string]any{"problems": problems}) | |||
} | |||
func CronTasks(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("admin.monitor.cron") | |||
ctx.Data["PageIsAdminMonitorCron"] = true |
@@ -4,8 +4,14 @@ | |||
package admin | |||
import ( | |||
"net/http" | |||
"testing" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/test" | |||
"code.gitea.io/gitea/services/contexttest" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
@@ -66,3 +72,21 @@ func TestShadowPassword(t *testing.T) { | |||
assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) | |||
} | |||
} | |||
func TestSelfCheckPost(t *testing.T) { | |||
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() | |||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")() | |||
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") | |||
SelfCheckPost(ctx) | |||
assert.EqualValues(t, http.StatusOK, resp.Code) | |||
data := struct { | |||
Problems []string `json:"problems"` | |||
}{} | |||
err := json.Unmarshal(resp.Body.Bytes(), &data) | |||
assert.NoError(t, err) | |||
assert.Equal(t, []string{ | |||
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"), | |||
}, data.Problems) | |||
} |
@@ -686,6 +686,7 @@ func registerRoutes(m *web.Route) { | |||
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) | |||
m.Get("/self_check", admin.SelfCheck) | |||
m.Post("/self_check", admin.SelfCheckPost) | |||
m.Group("/config", func() { | |||
m.Get("", admin.Config) |
@@ -309,7 +309,8 @@ func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, close | |||
Locale: middleware.Locale(resp, req), | |||
Data: middleware.GetContextData(req.Context()), | |||
} | |||
b.AppendContextValue(translation.ContextKey, b.Locale) | |||
b.Req = b.Req.WithContext(b) | |||
b.AppendContextValue(translation.ContextKey, b.Locale) | |||
b.AppendContextValue(httplib.RequestContextKey, b.Req) | |||
return b, b.cleanUp | |||
} |
@@ -39,7 +39,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { | |||
} | |||
requestURL, err := url.Parse(path) | |||
assert.NoError(t, err) | |||
req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} | |||
req := &http.Request{Method: method, Host: requestURL.Host, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} | |||
req = req.WithContext(middleware.WithContextData(req.Context())) | |||
return req | |||
} |
@@ -1,4 +1,4 @@ | |||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}} | |||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin")}} | |||
<div class="admin-setting-content"> | |||
<h4 class="ui top attached header"> | |||
@@ -6,7 +6,7 @@ | |||
</h4> | |||
{{if .StartupProblems}} | |||
<div class="ui attached segment"> | |||
<div class="ui attached segment self-check-problem"> | |||
<div class="ui warning message"> | |||
<div>{{ctx.Locale.Tr "admin.self_check.startup_warnings"}}</div> | |||
<ul class="tw-w-full">{{range .StartupProblems}}<li>{{.}}</li>{{end}}</ul> | |||
@@ -14,8 +14,10 @@ | |||
</div> | |||
{{end}} | |||
<div class="ui attached segment tw-hidden self-check-problem" id="self-check-by-frontend"></div> | |||
{{if .DatabaseCheckHasProblems}} | |||
<div class="ui attached segment"> | |||
<div class="ui attached segment self-check-problem"> | |||
{{if .DatabaseType.IsMySQL}} | |||
<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div> | |||
{{else if .DatabaseType.IsMSSQL}} | |||
@@ -29,22 +31,22 @@ | |||
{{end}} | |||
{{if .DatabaseCheckInconsistentCollationColumns}} | |||
<div class="ui red message"> | |||
{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}} | |||
<ul class="tw-w-full"> | |||
{{range .DatabaseCheckInconsistentCollationColumns}} | |||
<li>{{.}}</li> | |||
{{end}} | |||
</ul> | |||
<details> | |||
<summary>{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}</summary> | |||
<ul class="tw-w-full"> | |||
{{range .DatabaseCheckInconsistentCollationColumns}} | |||
<li>{{.}}</li> | |||
{{end}} | |||
</ul> | |||
</details> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
{{if and (not .StartupProblems) (not .DatabaseCheckHasProblems)}} | |||
<div class="ui attached segment"> | |||
{{/* only shown when there is no visible "self-check-problem" */}} | |||
<div class="ui attached segment tw-hidden self-check-no-problem"> | |||
{{ctx.Locale.Tr "admin.self_check.no_problem_found"}} | |||
</div> | |||
{{end}} | |||
</div> | |||
{{template "admin/layout_footer" .}} |
@@ -16,20 +16,20 @@ function shouldIgnoreError(err) { | |||
return false; | |||
} | |||
export function showGlobalErrorMessage(msg) { | |||
export function showGlobalErrorMessage(msg, msgType = 'error') { | |||
const msgContainer = document.querySelector('.page-content') ?? document.body; | |||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages | |||
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); | |||
if (!msgDiv) { | |||
const el = document.createElement('div'); | |||
el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`; | |||
el.innerHTML = `<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`; | |||
msgDiv = el.childNodes[0]; | |||
} | |||
// merge duplicated messages into "the message (count)" format | |||
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; | |||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); | |||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); | |||
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); | |||
msgDiv.querySelector('.ui.message').textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); | |||
msgContainer.prepend(msgDiv); | |||
} | |||
@@ -0,0 +1,31 @@ | |||
import {toggleElem} from '../../utils/dom.js'; | |||
import {POST} from '../../modules/fetch.js'; | |||
const {appSubUrl} = window.config; | |||
export async function initAdminSelfCheck() { | |||
const elCheckByFrontend = document.querySelector('#self-check-by-frontend'); | |||
if (!elCheckByFrontend) return; | |||
const elContent = document.querySelector('.page-content.admin .admin-setting-content'); | |||
// send frontend self-check request | |||
const resp = await POST(`${appSubUrl}/admin/self_check`, { | |||
data: new URLSearchParams({ | |||
location_origin: window.location.origin, | |||
now: Date.now(), // TODO: check time difference between server and client | |||
}), | |||
}); | |||
const json = await resp.json(); | |||
toggleElem(elCheckByFrontend, Boolean(json.problems?.length)); | |||
for (const problem of json.problems ?? []) { | |||
const elProblem = document.createElement('div'); | |||
elProblem.classList.add('ui', 'warning', 'message'); | |||
elProblem.textContent = problem; | |||
elCheckByFrontend.append(elProblem); | |||
} | |||
// only show the "no problem" if there is no visible "self-check-problem" | |||
const hasProblem = Boolean(elContent.querySelectorAll('.self-check-problem:not(.tw-hidden)').length); | |||
toggleElem(elContent.querySelector('.self-check-no-problem'), !hasProblem); | |||
} |
@@ -451,5 +451,5 @@ export function checkAppUrl() { | |||
return; | |||
} | |||
showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting. | |||
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`); | |||
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`, 'warning'); | |||
} |
@@ -87,6 +87,7 @@ import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js' | |||
import {initDirAuto} from './modules/dirauto.js'; | |||
import {initRepositorySearch} from './features/repo-search.js'; | |||
import {initColorPickers} from './features/colorpicker.js'; | |||
import {initAdminSelfCheck} from './features/admin/selfcheck.js'; | |||
// Init Gitea's Fomantic settings | |||
initGiteaFomantic(); | |||
@@ -132,6 +133,7 @@ onDomReady(() => { | |||
initAdminEmails(); | |||
initAdminUserListSearchForm(); | |||
initAdminConfigs(); | |||
initAdminSelfCheck(); | |||
initDashboardRepoList(); | |||