// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package install

import (
	"bytes"
	"errors"
	"image"
	_ "image/png"
	"io"
	"net/http"
	"net/mail"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"time"

	"code.gitea.io/gitea/models/db"
	db_install "code.gitea.io/gitea/models/db/install"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/auth/password/hash"
	"code.gitea.io/gitea/modules/generate"
	"code.gitea.io/gitea/modules/graceful"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/optional"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/templates"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/modules/typesniffer"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/modules/web/middleware"
	"code.gitea.io/gitea/routers/common"
	auth_service "code.gitea.io/gitea/services/auth"
	"code.gitea.io/gitea/services/context"
	"code.gitea.io/gitea/services/forms"
	"code.gitea.io/gitea/services/mailer"
	"code.gitea.io/gitea/services/versioned_migration"
)

const (
	// tplInstall template for installation page
	tplInstall     templates.TplName = "install"
	tplPostInstall templates.TplName = "post-install"

	registrationModeAdminOnly        = "admin_only"
	registrationModeLocalOnly        = "local_only"
	registrationModeExternalOnly     = "external_only"
	registrationModeLocalAndExternal = "local_and_external"

	installBrandingMaxFileSize = int64(1 << 20)
	installBrandingMinPNGEdge  = 64
	installAppINIImportMaxSize = int64(1 << 20)
)

type installBrandingAssetSpec struct {
	FormField  string
	TargetName string
	LabelKey   string
	MimeType   string
}

var installBrandingAssetSpecs = []installBrandingAssetSpec{
	{FormField: "logo_svg", TargetName: "logo.svg", LabelKey: "install.branding.logo_svg", MimeType: typesniffer.MimeTypeImageSvg},
	{FormField: "logo_png", TargetName: "logo.png", LabelKey: "install.branding.logo_png", MimeType: "image/png"},
	{FormField: "loading_png", TargetName: "loading.png", LabelKey: "install.branding.loading_png", MimeType: "image/png"},
	{FormField: "favicon_svg", TargetName: "favicon.svg", LabelKey: "install.branding.favicon_svg", MimeType: typesniffer.MimeTypeImageSvg},
	{FormField: "favicon_png", TargetName: "favicon.png", LabelKey: "install.branding.favicon_png", MimeType: "image/png"},
}

// getSupportedDbTypeNames returns a slice for supported database types and names. The slice is used to keep the order
func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
	for _, t := range setting.SupportedDatabaseTypes {
		dbTypeNames = append(dbTypeNames, map[string]string{"type": t, "name": setting.DatabaseTypeNames[t]})
	}
	return dbTypeNames
}

func resolveInstallDefaultLanguage(defaultLanguage string) string {
	if slices.Contains(setting.Langs, defaultLanguage) {
		return defaultLanguage
	}
	if len(setting.Langs) > 0 {
		return setting.Langs[0]
	}
	return ""
}

func reorderInstallLanguages(defaultLanguage string) []string {
	defaultLanguage = resolveInstallDefaultLanguage(defaultLanguage)
	if defaultLanguage == "" {
		return append([]string(nil), setting.Langs...)
	}

	langs := []string{defaultLanguage}
	for _, lang := range setting.Langs {
		if lang != defaultLanguage {
			langs = append(langs, lang)
		}
	}
	return langs
}

func resolveInstallRegistrationMode(disableRegistration, allowOnlyInternalRegistration, allowOnlyExternalRegistration bool) string {
	switch {
	case disableRegistration:
		return registrationModeAdminOnly
	case allowOnlyExternalRegistration:
		return registrationModeExternalOnly
	case allowOnlyInternalRegistration:
		return registrationModeLocalOnly
	default:
		return registrationModeLocalAndExternal
	}
}

func isLocalRegistrationMode(mode string) bool {
	return mode == registrationModeLocalOnly || mode == registrationModeLocalAndExternal
}

func isExternalRegistrationMode(mode string) bool {
	return mode == registrationModeExternalOnly || mode == registrationModeLocalAndExternal
}

func normalizeInstallRegistrationOptions(form *forms.InstallForm) {
	if form.RegistrationMode == "" {
		form.RegistrationMode = resolveInstallRegistrationMode(form.DisableRegistration, form.AllowOnlyInternalRegistration, form.AllowOnlyExternalRegistration)
	}

	switch form.RegistrationMode {
	case registrationModeAdminOnly:
		form.DisableRegistration = true
		form.AllowOnlyInternalRegistration = false
		form.AllowOnlyExternalRegistration = false
	case registrationModeLocalOnly:
		form.DisableRegistration = false
		form.AllowOnlyInternalRegistration = true
		form.AllowOnlyExternalRegistration = false
	case registrationModeExternalOnly:
		form.DisableRegistration = false
		form.AllowOnlyInternalRegistration = false
		form.AllowOnlyExternalRegistration = true
	default:
		form.RegistrationMode = registrationModeLocalAndExternal
		form.DisableRegistration = false
		form.AllowOnlyInternalRegistration = false
		form.AllowOnlyExternalRegistration = false
	}
	if form.AdminCreatedAccountMode == "" {
		if form.RegistrationMode == registrationModeAdminOnly {
			form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeInvite
		} else {
			form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeLocal
		}
	}
	if form.AdminCreatedAccountMode != setting.AdminCreatedAccountModeInvite {
		form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeLocal
	}

	if !isLocalRegistrationMode(form.RegistrationMode) {
		form.RegisterConfirm = false
		form.RegisterManualConfirm = false
		form.EnableCaptcha = false
	}
	if form.RegisterManualConfirm {
		form.RegisterConfirm = true
	}

	if !isExternalRegistrationMode(form.RegistrationMode) {
		form.EnableOpenIDSignIn = false
		form.EnableOpenIDSignUp = false
	}
	if form.EnableOpenIDSignUp {
		form.EnableOpenIDSignIn = true
	}
}

func collectInstallBrandingAssets(ctx *context.Context, useSharedAssets bool) (map[string][]byte, error) {
	uploads := make(map[string][]byte)

	for _, spec := range installBrandingAssetSpecs {
		file, header, err := ctx.Req.FormFile(spec.FormField)
		if errors.Is(err, http.ErrMissingFile) {
			continue
		}
		if err != nil {
			return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
		}
		data, err := func() ([]byte, error) {
			defer file.Close()

			if header.Size > installBrandingMaxFileSize {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
			}

			data, err := io.ReadAll(io.LimitReader(file, installBrandingMaxFileSize+1))
			if err != nil {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
			}
			if int64(len(data)) > installBrandingMaxFileSize {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
			}

			return data, nil
		}()
		if err != nil {
			return nil, err
		}

		sniffedType := typesniffer.DetectContentType(data)
		switch spec.MimeType {
		case typesniffer.MimeTypeImageSvg:
			if !sniffedType.IsSvgImage() {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "SVG")))
			}
		case "image/png":
			if sniffedType.GetMimeType() != "image/png" {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
			}

			cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
			if err != nil || format != "png" {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
			}
			if cfg.Width != cfg.Height {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_png_square", ctx.Tr(spec.LabelKey))))
			}
			if cfg.Width < installBrandingMinPNGEdge || cfg.Height < installBrandingMinPNGEdge {
				return nil, errors.New(string(ctx.Tr("install.branding.upload_png_too_small", ctx.Tr(spec.LabelKey), installBrandingMinPNGEdge, installBrandingMinPNGEdge)))
			}
		}

		uploads[spec.TargetName] = data
	}

	if useSharedAssets {
		if logoSVG, ok := uploads["logo.svg"]; ok {
			uploads["favicon.svg"] = logoSVG
		}
		if logoPNG, ok := uploads["logo.png"]; ok {
			uploads["favicon.png"] = logoPNG
		}
	}

	return uploads, nil
}

func saveInstallBrandingAssets(uploads map[string][]byte) error {
	if len(uploads) == 0 {
		return nil
	}

	customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
	if err := os.MkdirAll(customImgPath, os.ModePerm); err != nil {
		return err
	}

	for targetName, data := range uploads {
		if err := os.WriteFile(filepath.Join(customImgPath, targetName), data, 0o644); err != nil {
			return err
		}
	}

	if logoSVG, ok := uploads["logo.svg"]; ok {
		if err := os.WriteFile(filepath.Join(customImgPath, "gitea.svg"), logoSVG, 0o644); err != nil {
			return err
		}
	}

	return nil
}

func getInstallProgressLogo() string {
	customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
	if _, err := os.Stat(filepath.Join(customImgPath, "loading.png")); err == nil {
		return "loading.png"
	}
	if _, err := os.Stat(filepath.Join(customImgPath, "logo.svg")); err == nil {
		return "logo.svg"
	}
	if _, err := os.Stat(filepath.Join(customImgPath, "logo.png")); err == nil {
		return "logo.png"
	}
	return "loading.png"
}

func installContexter() func(next http.Handler) http.Handler {
	return context.ContexterInstallPage(map[string]any{
		"DbTypeNames":            getSupportedDbTypeNames(),
		"AppINIImportMaxSizeKB":  installAppINIImportMaxSize / 1024,
		"BrandingMaxFileSizeKB":  installBrandingMaxFileSize / 1024,
		"BrandingMinPNGEdge":     installBrandingMinPNGEdge,
		"EnvConfigKeys":          setting.CollectEnvConfigKeys(),
		"CustomConfFile":         setting.CustomConf,
		"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
	})
}

func newInstallFormFromSettings() (forms.InstallForm, string) {
	form := forms.InstallForm{}
	form.BrandingUseSharedAssets = true
	// Database settings
	form.DbHost = setting.Database.Host
	form.DbUser = setting.Database.User
	form.DbPasswd = setting.Database.Passwd
	form.DbName = setting.Database.Name
	form.DbPath = setting.Database.Path
	form.DbSchema = setting.Database.Schema
	form.SSLMode = setting.Database.SSLMode

	curDBType := setting.Database.Type.String()
	if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) {
		curDBType = "mysql"
	}

	// Application general settings
	form.AppName = setting.AppName
	form.DefaultLanguage = resolveInstallDefaultLanguage("")
	form.RepoRootPath = setting.RepoRootPath
	form.LFSRootPath = setting.LFS.Storage.Path
	form.RunUser = setting.RunUser
	form.Domain = setting.Domain
	form.SSHPort = setting.SSH.Port
	form.HTTPPort = setting.HTTPPort
	form.AppURL = setting.AppURL
	form.LogRootPath = setting.Log.RootPath

	// E-mail service settings
	if setting.MailService != nil {
		form.SMTPAddr = setting.MailService.SMTPAddr
		form.SMTPPort = setting.MailService.SMTPPort
		form.SMTPFrom = setting.MailService.From
		form.SMTPUser = setting.MailService.User
		form.SMTPPasswd = setting.MailService.Passwd
	}
	form.RegisterConfirm = setting.Service.RegisterEmailConfirm
	form.RegisterManualConfirm = setting.Service.RegisterManualConfirm
	form.MailNotify = setting.Service.EnableNotifyMail
	if setting.MailService == nil && !setting.Service.EnableNotifyMail {
		form.MailNotify = true
	}

	form.RegistrationMode = resolveInstallRegistrationMode(setting.Service.DisableRegistration, setting.Service.AllowOnlyInternalRegistration, setting.Service.AllowOnlyExternalRegistration)
	form.AdminCreatedAccountMode = setting.Service.AdminCreatedAccountMode
	form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
	form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
	form.DisableRegistration = setting.Service.DisableRegistration
	form.AllowOnlyInternalRegistration = setting.Service.AllowOnlyInternalRegistration
	form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
	form.EnableCaptcha = setting.Service.EnableCaptcha
	form.RequireSignInView = setting.Service.RequireSignInViewStrict
	form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
	form.NoReplyAddress = setting.Service.NoReplyAddress
	form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
	form.AdminManagementPolicy = setting.Admin.AdminManagementPolicy
	normalizeInstallRegistrationOptions(&form)

	return form, curDBType
}

func renderInstallPage(ctx *context.Context, form *forms.InstallForm, curDBType string) {
	if form.DefaultLanguage == "" {
		form.DefaultLanguage = resolveInstallDefaultLanguage("")
	}
	normalizeInstallRegistrationOptions(form)
	ctx.Data["CurDbType"] = curDBType
	middleware.AssignForm(form, ctx.Data)
	ctx.HTML(http.StatusOK, tplInstall)
}

func importedDBType(dbType string, fallback string) string {
	dbType = strings.TrimSpace(strings.ToLower(dbType))
	if slices.Contains(setting.SupportedDatabaseTypes, dbType) {
		return dbType
	}
	return fallback
}

func importedInt(sec setting.ConfigSection, key string, fallback int) int {
	cfgKey := setting.ConfigSectionKey(sec, key)
	if cfgKey == nil {
		return fallback
	}
	return cfgKey.MustInt(fallback)
}

func importedFirstLang(value string, fallback string) string {
	for _, lang := range strings.Split(value, ",") {
		lang = strings.TrimSpace(lang)
		if lang != "" {
			return resolveInstallDefaultLanguage(lang)
		}
	}
	return resolveInstallDefaultLanguage(fallback)
}

func populateInstallFormFromConfig(form *forms.InstallForm, cfg setting.ConfigProvider, fallbackDBType string) string {
	rootSec := cfg.Section("")
	dbSec := cfg.Section("database")
	repoSec := cfg.Section("repository")
	serverSec := cfg.Section("server")
	lfsSec := cfg.Section("lfs")
	logSec := cfg.Section("log")
	mailerSec := cfg.Section("mailer")
	serviceSec := cfg.Section("service")
	openIDSec := cfg.Section("openid")
	securitySec := cfg.Section("security")
	adminSec := cfg.Section("admin")
	i18nSec := cfg.Section("i18n")

	form.AppName = setting.ConfigSectionKeyString(rootSec, "APP_NAME", form.AppName)
	form.RunUser = setting.ConfigSectionKeyString(rootSec, "RUN_USER", form.RunUser)

	form.DbType = importedDBType(setting.ConfigSectionKeyString(dbSec, "DB_TYPE", fallbackDBType), fallbackDBType)
	form.DbHost = setting.ConfigSectionKeyString(dbSec, "HOST", form.DbHost)
	form.DbUser = setting.ConfigSectionKeyString(dbSec, "USER", form.DbUser)
	form.DbPasswd = setting.ConfigSectionKeyString(dbSec, "PASSWD", form.DbPasswd)
	form.DbName = setting.ConfigSectionKeyString(dbSec, "NAME", form.DbName)
	form.DbSchema = setting.ConfigSectionKeyString(dbSec, "SCHEMA", form.DbSchema)
	form.SSLMode = setting.ConfigSectionKeyString(dbSec, "SSL_MODE", form.SSLMode)
	form.DbPath = setting.ConfigSectionKeyString(dbSec, "PATH", form.DbPath)

	form.DefaultLanguage = importedFirstLang(setting.ConfigSectionKeyString(i18nSec, "LANGS"), form.DefaultLanguage)
	form.RepoRootPath = setting.ConfigSectionKeyString(repoSec, "ROOT", form.RepoRootPath)
	form.LFSRootPath = setting.ConfigSectionKeyString(lfsSec, "PATH", form.LFSRootPath)
	form.Domain = setting.ConfigSectionKeyString(serverSec, "DOMAIN", setting.ConfigSectionKeyString(serverSec, "SSH_DOMAIN", form.Domain))
	form.SSHPort = importedInt(serverSec, "SSH_PORT", form.SSHPort)
	if setting.ConfigSectionKeyBool(serverSec, "DISABLE_SSH") {
		form.SSHPort = 0
	}
	form.HTTPPort = setting.ConfigSectionKeyString(serverSec, "HTTP_PORT", form.HTTPPort)
	form.AppURL = setting.ConfigSectionKeyString(serverSec, "ROOT_URL", form.AppURL)
	form.LogRootPath = setting.ConfigSectionKeyString(logSec, "ROOT_PATH", form.LogRootPath)

	if setting.ConfigSectionKeyBool(mailerSec, "ENABLED") {
		form.SMTPAddr = setting.ConfigSectionKeyString(mailerSec, "SMTP_ADDR", form.SMTPAddr)
		form.SMTPPort = setting.ConfigSectionKeyString(mailerSec, "SMTP_PORT", form.SMTPPort)
		form.SMTPFrom = setting.ConfigSectionKeyString(mailerSec, "FROM", form.SMTPFrom)
		form.SMTPUser = setting.ConfigSectionKeyString(mailerSec, "USER", form.SMTPUser)
		form.SMTPPasswd = setting.ConfigSectionKeyString(mailerSec, "PASSWD", form.SMTPPasswd)
	}

	form.RegisterConfirm = setting.ConfigSectionKeyBool(serviceSec, "REGISTER_EMAIL_CONFIRM", form.RegisterConfirm)
	form.RegisterManualConfirm = setting.ConfigSectionKeyBool(serviceSec, "REGISTER_MANUAL_CONFIRM", form.RegisterManualConfirm)
	form.MailNotify = setting.ConfigSectionKeyBool(serviceSec, "ENABLE_NOTIFY_MAIL", form.MailNotify)
	form.AdminCreatedAccountMode = setting.ConfigSectionKeyString(serviceSec, "ADMIN_CREATED_ACCOUNT_MODE", form.AdminCreatedAccountMode)
	form.DisableRegistration = setting.ConfigSectionKeyBool(serviceSec, "DISABLE_REGISTRATION", form.DisableRegistration)
	form.AllowOnlyInternalRegistration = setting.ConfigSectionKeyBool(serviceSec, "ALLOW_ONLY_INTERNAL_REGISTRATION", form.AllowOnlyInternalRegistration)
	form.AllowOnlyExternalRegistration = setting.ConfigSectionKeyBool(serviceSec, "ALLOW_ONLY_EXTERNAL_REGISTRATION", form.AllowOnlyExternalRegistration)
	form.RegistrationMode = ""
	form.EnableCaptcha = setting.ConfigSectionKeyBool(serviceSec, "ENABLE_CAPTCHA", form.EnableCaptcha)
	form.RequireSignInView = setting.ConfigSectionKeyBool(serviceSec, "REQUIRE_SIGNIN_VIEW", form.RequireSignInView)
	form.DefaultKeepEmailPrivate = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_KEEP_EMAIL_PRIVATE", form.DefaultKeepEmailPrivate)
	form.DefaultAllowCreateOrganization = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_ALLOW_CREATE_ORGANIZATION", form.DefaultAllowCreateOrganization)
	form.DefaultEnableTimetracking = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_ENABLE_TIMETRACKING", form.DefaultEnableTimetracking)
	form.NoReplyAddress = setting.ConfigSectionKeyString(serviceSec, "NO_REPLY_ADDRESS", form.NoReplyAddress)

	form.EnableOpenIDSignIn = setting.ConfigSectionKeyBool(openIDSec, "ENABLE_OPENID_SIGNIN", form.EnableOpenIDSignIn)
	form.EnableOpenIDSignUp = setting.ConfigSectionKeyBool(openIDSec, "ENABLE_OPENID_SIGNUP", form.EnableOpenIDSignUp)
	form.EnableUpdateChecker = setting.ConfigSectionKeyBool(securitySec, "ENABLE_UPDATE_CHECKER", form.EnableUpdateChecker)
	form.PasswordAlgorithm = setting.ConfigSectionKeyString(securitySec, "PASSWORD_HASH_ALGO", form.PasswordAlgorithm)
	form.AdminManagementPolicy = setting.ConfigSectionKeyString(adminSec, "ADMIN_MANAGEMENT_POLICY", form.AdminManagementPolicy)
	if form.ImportSensitiveSecrets {
		form.ImportedLFSJWTSecret = readImportedSecretValue(serverSec, "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
		form.ImportedInternalToken = readImportedSecretValue(securitySec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
		form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
	}
	normalizeInstallRegistrationOptions(form)

	return form.DbType
}

func readImportedSecretValue(sec setting.ConfigSection, uriKey, verbatimKey string) string {
	verbatim := setting.ConfigSectionKeyString(sec, verbatimKey)
	if verbatim != "" {
		return verbatim
	}

	uriValue := setting.ConfigSectionKeyString(sec, uriKey)
	if uriValue == "" {
		return ""
	}

	parsed, err := url.Parse(uriValue)
	if err != nil || parsed.Scheme != "file" {
		return ""
	}

	path := parsed.Path
	if parsed.Opaque != "" {
		path = parsed.Opaque
	}
	if parsed.Host != "" {
		switch {
		case len(parsed.Host) == 2 && parsed.Host[1] == ':':
			path = parsed.Host + path
		default:
			path = "//" + parsed.Host + path
		}
	}
	path, err = url.PathUnescape(path)
	if err != nil || path == "" {
		return ""
	}
	if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") && len(path) > 2 && path[2] == ':' {
		path = path[1:]
	}

	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

func readImportedInstallConfig(ctx *context.Context) (setting.ConfigProvider, error) {
	file, header, err := ctx.Req.FormFile("app_ini_file")
	if errors.Is(err, http.ErrMissingFile) {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_missing")))
	}
	if err != nil {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_read_failed", err)))
	}
	defer file.Close()

	if header.Size > installAppINIImportMaxSize {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_too_big", installAppINIImportMaxSize/1024)))
	}

	data, err := io.ReadAll(io.LimitReader(file, installAppINIImportMaxSize+1))
	if err != nil {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_read_failed", err)))
	}
	if int64(len(data)) > installAppINIImportMaxSize {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_too_big", installAppINIImportMaxSize/1024)))
	}

	cfg, err := setting.NewConfigProviderFromData(string(data))
	if err != nil {
		return nil, errors.New(string(ctx.Tr("install.import_app_ini_invalid", err)))
	}
	return cfg, nil
}

func populateInstallSensitiveSecretsFromConfig(form *forms.InstallForm, cfg setting.ConfigProvider) {
	if !form.ImportSensitiveSecrets {
		return
	}
	if form.ImportedLFSJWTSecret == "" {
		form.ImportedLFSJWTSecret = readImportedSecretValue(cfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
	}
	if form.ImportedInternalToken == "" {
		form.ImportedInternalToken = readImportedSecretValue(cfg.Section("security"), "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
	}
	if form.ImportedOAuth2JWTSecret == "" {
		form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
	}
}

func applyInstallSensitiveSecretsToConfig(cfg setting.ConfigProvider, form *forms.InstallForm) error {
	if form.LFSRootPath != "" {
		if form.ImportSensitiveSecrets && form.ImportedLFSJWTSecret != "" {
			cfg.Section("server").DeleteKey("LFS_JWT_SECRET_URI")
			cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(form.ImportedLFSJWTSecret)
		} else if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
			_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
			cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
		}
	}

	// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
	// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
	if form.ImportSensitiveSecrets && form.ImportedInternalToken != "" {
		cfg.Section("security").DeleteKey("INTERNAL_TOKEN_URI")
		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(form.ImportedInternalToken)
	} else if setting.InternalToken == "" {
		internalToken, err := generate.NewInternalToken()
		if err != nil {
			return err
		}
		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
	}

	// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
	// see the "loadOAuth2From" in "setting/oauth2.go"
	if form.ImportSensitiveSecrets && form.ImportedOAuth2JWTSecret != "" {
		cfg.Section("oauth2").DeleteKey("JWT_SECRET_URI")
		cfg.Section("oauth2").Key("JWT_SECRET").SetValue(form.ImportedOAuth2JWTSecret)
	} else if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
		_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
		cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
	}

	return nil
}

// Install render installation page
func Install(ctx *context.Context) {
	if setting.InstallLock {
		InstallDone(ctx)
		return
	}

	form, curDBType := newInstallFormFromSettings()
	renderInstallPage(ctx, &form, curDBType)
}

func ImportAppINI(ctx *context.Context) {
	if setting.InstallLock {
		InstallDone(ctx)
		return
	}

	form, curDBType := newInstallFormFromSettings()
	form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
	cfg, err := readImportedInstallConfig(ctx)
	if err != nil {
		ctx.Data["CurDbType"] = curDBType
		ctx.RenderWithErrDeprecated(err.Error(), tplInstall, &form)
		return
	}

	curDBType = populateInstallFormFromConfig(&form, cfg, curDBType)
	ctx.Flash.Success(ctx.Tr("install.import_app_ini_success"), true)
	renderInstallPage(ctx, &form, curDBType)
}

// TestMail checks the mail settings entered on the install page.
func TestMail(ctx *context.Context) {
	email := strings.TrimSpace(ctx.FormString("test_mail_email"))
	if email == "" {
		email = strings.TrimSpace(ctx.FormString("admin_email"))
	}

	state := "success"
	message := string(ctx.Tr("admin.config.test_mail_sent", email))
	ok := true

	mailService, err := buildInstallTestMailService(ctx)
	if err != nil {
		state = "failed"
		message = err.Error()
		ok = false
	} else if err := mailer.SendTestMailWith(email, mailService); err != nil {
		state = "failed"
		message = string(ctx.Tr("admin.config.test_mail_failed", email, mailer.ShortTestMailError(err)))
		ok = false
	}

	ctx.JSON(http.StatusOK, map[string]any{
		"ok":      ok,
		"state":   state,
		"message": message,
		"email":   email,
	})
}

func buildInstallTestMailService(ctx *context.Context) (*setting.Mailer, error) {
	smtpAddr := strings.TrimSpace(ctx.FormString("smtp_addr"))
	smtpPort := strings.TrimSpace(ctx.FormString("smtp_port"))
	smtpFrom := strings.TrimSpace(ctx.FormString("smtp_from"))

	if smtpAddr == "" {
		return nil, errors.New(string(ctx.Tr("install.test_mail_missing_host")))
	}

	email := strings.TrimSpace(ctx.FormString("test_mail_email"))
	if email == "" {
		email = strings.TrimSpace(ctx.FormString("admin_email"))
	}
	if email == "" {
		return nil, errors.New(string(ctx.Tr("install.test_mail_missing_recipient")))
	}
	if _, err := mail.ParseAddress(email); err != nil {
		return nil, errors.New(string(ctx.Tr("form.email_invalid")))
	}

	parsedFrom, err := mail.ParseAddress(smtpFrom)
	if err != nil {
		return nil, errors.New(string(ctx.Tr("install.smtp_from_invalid")))
	}

	protocol := inferInstallMailProtocol(smtpAddr, smtpPort)
	if smtpPort == "" {
		switch protocol {
		case "smtp":
			smtpPort = "25"
		case "smtp+starttls":
			smtpPort = "587"
		default:
			smtpPort = "465"
		}
	}

	return &setting.Mailer{
		Name:           setting.AppName,
		From:           smtpFrom,
		FromName:       parsedFrom.Name,
		FromEmail:      parsedFrom.Address,
		Protocol:       protocol,
		SMTPAddr:       smtpAddr,
		SMTPPort:       smtpPort,
		User:           strings.TrimSpace(ctx.FormString("smtp_user")),
		Passwd:         ctx.FormString("smtp_passwd"),
		EnableHelo:     true,
		OverrideHeader: map[string][]string{},
	}, nil
}

func inferInstallMailProtocol(smtpAddr, smtpPort string) string {
	if strings.ContainsAny(smtpAddr, "/\\") {
		return "smtp+unix"
	}
	switch smtpPort {
	case "25":
		return "smtp"
	case "587":
		return "smtp+starttls"
	default:
		return "smtps"
	}
}

func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
	var err error

	if (setting.Database.Type == "sqlite3") &&
		len(setting.Database.Path) == 0 {
		ctx.Data["Err_DbPath"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form)
		return false
	}

	// Check if the user is trying to re-install in an installed database
	db.UnsetDefaultEngine()
	defer db.UnsetDefaultEngine()

	if err = db.InitEngine(ctx); err != nil {
		if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
			ctx.Data["Err_DbType"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form)
		} else {
			ctx.Data["Err_DbSetting"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
		}
		return false
	}

	err = db_install.CheckDatabaseConnection(ctx)
	if err != nil {
		ctx.Data["Err_DbSetting"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
		return false
	}

	hasPostInstallationUser, err := db_install.HasPostInstallationUsers(ctx)
	if err != nil {
		ctx.Data["Err_DbSetting"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form)
		return false
	}
	dbMigrationVersion, err := db_install.GetMigrationVersion(ctx)
	if err != nil {
		ctx.Data["Err_DbSetting"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form)
		return false
	}

	if hasPostInstallationUser && dbMigrationVersion > 0 {
		log.Error("The database is likely to have been used by Gitea before, database migration version=%d", dbMigrationVersion)
		confirmed := form.ReinstallConfirmFirst && form.ReinstallConfirmSecond && form.ReinstallConfirmThird
		if !confirmed {
			ctx.Data["Err_DbInstalledBefore"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form)
			return false
		}

		log.Info("User confirmed re-installation of Gitea into a pre-existing database")
	}

	if hasPostInstallationUser || dbMigrationVersion > 0 {
		log.Info("Gitea will be installed in a database with: hasPostInstallationUser=%v, dbMigrationVersion=%v", hasPostInstallationUser, dbMigrationVersion)
	}

	return true
}

// SubmitInstall response for submit install items
func SubmitInstall(ctx *context.Context) {
	if setting.InstallLock {
		InstallDone(ctx)
		return
	}

	var err error

	form := *web.GetForm(ctx).(*forms.InstallForm)
	form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
	form.ImportedLFSJWTSecret = ctx.FormString("imported_lfs_jwt_secret")
	form.ImportedInternalToken = ctx.FormString("imported_internal_token")
	form.ImportedOAuth2JWTSecret = ctx.FormString("imported_o_auth2_jwt_secret")
	if form.ImportSensitiveSecrets && (form.ImportedLFSJWTSecret == "" || form.ImportedInternalToken == "" || form.ImportedOAuth2JWTSecret == "") {
		if importedCfg, err := readImportedInstallConfig(ctx); err == nil {
			populateInstallSensitiveSecretsFromConfig(&form, importedCfg)
		}
	}

	// fix form values
	if form.AppURL != "" && form.AppURL[len(form.AppURL)-1] != '/' {
		form.AppURL += "/"
	}
	if form.AdminManagementPolicy == "" {
		form.AdminManagementPolicy = setting.AdminManagementPolicyGrantorOnly
	}
	form.DefaultLanguage = strings.TrimSpace(form.DefaultLanguage)
	normalizeInstallRegistrationOptions(&form)

	if form.DefaultLanguage == "" {
		form.DefaultLanguage = resolveInstallDefaultLanguage("")
	} else if !slices.Contains(setting.Langs, form.DefaultLanguage) {
		ctx.Data["Err_DefaultLanguage"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("form.lang_select_error"), tplInstall, &form)
		return
	}

	ctx.Data["CurDbType"] = form.DbType

	if ctx.HasError() {
		ctx.Data["Err_SMTP"] = ctx.Data["Err_SMTPUser"] != nil
		ctx.Data["Err_Registration"] = ctx.Data["Err_RegistrationMode"] != nil
		ctx.Data["Err_Admin"] = ctx.Data["Err_AdminName"] != nil || ctx.Data["Err_AdminPasswd"] != nil || ctx.Data["Err_AdminEmail"] != nil
		ctx.HTML(http.StatusOK, tplInstall)
		return
	}

	if _, err = exec.LookPath("git"); err != nil {
		ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
		return
	}

	// ---- Basic checks are passed, now test configuration.

	// Test database setting.
	setting.Database.Type = setting.DatabaseType(form.DbType)
	setting.Database.Host = form.DbHost
	setting.Database.User = form.DbUser
	setting.Database.Passwd = form.DbPasswd
	setting.Database.Name = form.DbName
	setting.Database.Schema = form.DbSchema
	setting.Database.SSLMode = form.SSLMode
	setting.Database.Path = form.DbPath
	setting.Database.LogSQL = !setting.IsProd

	if !checkDatabase(ctx, &form) {
		return
	}

	// Prepare AppDataPath, it is very important for Gitea
	if err = setting.PrepareAppDataPath(); err != nil {
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form)
		return
	}

	// Test repository root path.
	form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
	if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
		ctx.Data["Err_RepoRootPath"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
		return
	}

	// Test LFS root path if not empty, empty meaning disable LFS
	if form.LFSRootPath != "" {
		form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
		if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
			ctx.Data["Err_LFSRootPath"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
			return
		}
	}

	// Test log root path.
	form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
	if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
		ctx.Data["Err_LogRootPath"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
		return
	}

	// Check logic loophole between disable self-registration and no admin account.
	if form.DisableRegistration && len(form.AdminName) == 0 {
		ctx.Data["Err_Registration"] = true
		ctx.Data["Err_Admin"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
		return
	}

	// Check admin user creation
	if len(form.AdminName) > 0 {
		// Ensure AdminName is valid
		if err := user_model.IsUsableUsername(form.AdminName); err != nil {
			ctx.Data["Err_Admin"] = true
			ctx.Data["Err_AdminName"] = true
			if db.IsErrNameReserved(err) {
				ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
				return
			} else if db.IsErrNamePatternNotAllowed(err) {
				ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
				return
			}
			ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
			return
		}
		// Check Admin email
		if len(form.AdminEmail) == 0 {
			ctx.Data["Err_Admin"] = true
			ctx.Data["Err_AdminEmail"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
			return
		}
		// Check admin password.
		if len(form.AdminPasswd) == 0 {
			ctx.Data["Err_Admin"] = true
			ctx.Data["Err_AdminPasswd"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
			return
		}
		if form.AdminPasswd != form.AdminConfirmPasswd {
			ctx.Data["Err_Admin"] = true
			ctx.Data["Err_AdminPasswd"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplInstall, form)
			return
		}
	}

	brandingUploads, err := collectInstallBrandingAssets(ctx, form.BrandingUseSharedAssets)
	if err != nil {
		ctx.Data["Err_Branding"] = true
		ctx.RenderWithErrDeprecated(err.Error(), tplInstall, form)
		return
	}

	// Init the engine with migration
	if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
		db.UnsetDefaultEngine()
		ctx.Data["Err_DbSetting"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
		return
	}

	// Save settings.
	cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
	if err != nil {
		log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
	}

	cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
	cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
	cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
	cfg.Section("").Key("RUN_MODE").SetValue("prod")

	cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String())
	cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
	cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
	cfg.Section("database").Key("USER").SetValue(setting.Database.User)
	cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
	cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
	cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
	cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
	cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful

	cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
	cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
	cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
	cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
	cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
	cfg.Section("server").Key("APP_DATA_PATH").SetValue(setting.AppDataPath)

	if form.SSHPort == 0 {
		cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
	} else {
		cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
		cfg.Section("server").Key("SSH_PORT").SetValue(strconv.Itoa(form.SSHPort))
	}

	if form.LFSRootPath != "" {
		cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
		cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
	} else {
		cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
	}

	if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
		if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)
			return
		}

		cfg.Section("mailer").Key("ENABLED").SetValue("true")
		cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
		cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)
		cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
		cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
		cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
	} else {
		cfg.Section("mailer").Key("ENABLED").SetValue("false")
	}
	registerEmailConfirm := form.RegisterConfirm && !form.RegisterManualConfirm && isLocalRegistrationMode(form.RegistrationMode)
	registerManualConfirm := form.RegisterManualConfirm && isLocalRegistrationMode(form.RegistrationMode)

	cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(registerEmailConfirm))
	cfg.Section("service").Key("REGISTER_MANUAL_CONFIRM").SetValue(strconv.FormatBool(registerManualConfirm))
	cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify))
	cfg.Section("service").Key("ADMIN_CREATED_ACCOUNT_MODE").SetValue(form.AdminCreatedAccountMode)

	cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn))
	cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp))
	cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration))
	cfg.Section("service").Key("ALLOW_ONLY_INTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyInternalRegistration))
	cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyExternalRegistration))
	cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(strconv.FormatBool(form.EnableCaptcha))
	cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
	cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
	cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
	cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
	cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
	cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
	cfg.Section("admin").Key("SUPER_ADMIN_ENABLED").SetValue("true")
	cfg.Section("admin").Key("ADMIN_MANAGEMENT_POLICY").SetValue(form.AdminManagementPolicy)
	cfg.Section("i18n").Key("LANGS").SetValue(strings.Join(reorderInstallLanguages(form.DefaultLanguage), ","))

	cfg.Section("session").Key("PROVIDER").SetValue("file")

	cfg.Section("log").Key("MODE").MustString("console")
	cfg.Section("log").Key("LEVEL").SetValue(setting.Log.Level.String())
	cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)

	cfg.Section("repository.pull-request").Key("DEFAULT_MERGE_STYLE").SetValue("merge")

	cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer")

	cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
	if err = applyInstallSensitiveSecretsToConfig(cfg, &form); err != nil {
		ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
		return
	}

	// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
	if setting.SecretKey == "" {
		var secretKey string
		if secretKey, err = generate.NewSecretKey(); err != nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
			return
		}
		cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
	}

	if len(form.PasswordAlgorithm) > 0 {
		var algorithm *hash.PasswordHashAlgorithm
		setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm)
		if algorithm == nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form)
			return
		}
		cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
	}

	log.Info("Save settings to custom config file %s", setting.CustomConf)

	err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
	if err != nil {
		ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
		return
	}

	setting.EnvironmentToConfig(cfg, os.Environ())

	if err = cfg.SaveTo(setting.CustomConf); err != nil {
		ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
		return
	}

	if err = saveInstallBrandingAssets(brandingUploads); err != nil {
		ctx.Data["Err_Branding"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("install.branding.upload_save_failed", err), tplInstall, &form)
		return
	}

	// unset default engine before reload database setting
	db.UnsetDefaultEngine()

	// ---- All checks are passed

	// Reload settings (and re-initialize database connection)
	setting.InitCfgProvider(setting.CustomConf)
	setting.LoadCommonSettings()
	setting.MustInstalled()
	setting.LoadDBSetting()
	if err := common.InitDBEngine(ctx); err != nil {
		log.Fatal("ORM engine initialization failed: %v", err)
	}

	// Create admin account
	if len(form.AdminName) > 0 {
		u := &user_model.User{
			Name:    form.AdminName,
			Email:   form.AdminEmail,
			Passwd:  form.AdminPasswd,
			IsAdmin: true,
		}
		overwriteDefault := &user_model.CreateUserOverwriteOptions{
			IsRestricted: optional.Some(false),
			IsActive:     optional.Some(true),
		}

		if err = user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
			if !user_model.IsErrUserAlreadyExist(err) {
				setting.InstallLock = false
				ctx.Data["Err_AdminName"] = true
				ctx.Data["Err_AdminEmail"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
				return
			}
			log.Info("Admin account already exist")
			u, _ = user_model.GetUserByName(ctx, u.Name)
		}

		nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
		if err != nil {
			ctx.ServerError("CreateAuthTokenForUserID", err)
			return
		}

		ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)

		// Auto-login for admin
		if err = ctx.Session.Set("uid", u.ID); err != nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
			return
		}
		if err = ctx.Session.Set("uname", u.Name); err != nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
			return
		}

		if err = ctx.Session.Release(); err != nil {
			ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
			return
		}
	}

	setting.ClearEnvConfigKeys()
	log.Info("First-time run install finished!")
	InstallDone(ctx)

	go func() {
		// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
		// What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future ....
		time.Sleep(3 * time.Second)

		// Now get the http.Server from this request and shut it down
		// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
		srv := ctx.Value(http.ServerContextKey).(*http.Server)
		if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
			log.Error("Unable to shutdown the install server! Error: %v", err)
		}

		// After the HTTP server for "install" shuts down, the `runWeb()` will continue to run the "normal" server
	}()
}

// InstallDone shows the "post-install" page, makes it easier to develop the page.
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
func InstallDone(ctx *context.Context) { //nolint:revive // export stutter
	hasUsers, _ := user_model.HasUsers(ctx)
	ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
	ctx.Data["InstallProgressLogo"] = getInstallProgressLogo()
	ctx.HTML(http.StatusOK, tplPostInstall)
}
