gitea/services/pages/pages.go
Admin 69d7c72ba8
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m32s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m58s
feat(pages): Add subdomain routing and default config support
- Update subdomain parser to use {repo}-{owner}.{domain} format
- Add middleware to intercept Pages subdomain requests
- Generate default config when Pages enabled but no .gitea/landing.yaml
- Pages are public landing pages (accessible even for private repos)

🤖 Generated with Claude Code
2026-01-11 00:15:21 +00:00

306 lines
8.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pages
import (
"context"
"errors"
"fmt"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
pages_module "code.gitea.io/gitea/modules/pages"
"code.gitea.io/gitea/modules/setting"
)
const (
// LandingConfigPath is the path to the landing page configuration file
LandingConfigPath = ".gitea/landing.yaml"
// LandingConfigPathAlt is an alternative path for the configuration file
LandingConfigPathAlt = ".gitea/landing.yml"
)
// GetPagesConfig returns the pages configuration for a repository
// It first tries to load from the database cache, then falls back to parsing the file
func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, error) {
// Check if pages is configured in DB
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
if err != nil {
return nil, err
}
// Try to load and parse the config from the repository
fileConfig, fileHash, err := loadConfigFromRepo(ctx, repo)
if err != nil {
// If file doesn't exist, check if DB has config
if dbConfig != nil && dbConfig.ConfigJSON != "" {
// Use cached config
var config pages_module.LandingConfig
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
return nil, err
}
return &config, nil
}
// If Pages is enabled but no config file, return a default config
if dbConfig != nil && dbConfig.Enabled {
return getDefaultConfig(repo, string(dbConfig.Template)), nil
}
return nil, err
}
// If we have a file config, check if cache needs updating
if dbConfig != nil && dbConfig.ConfigHash == fileHash {
// Cache is up to date, use cached config
var config pages_module.LandingConfig
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
// Cache is corrupted, use file config
return fileConfig, nil
}
return &config, nil
}
// Update cache with new config
configJSON, err := json.Marshal(fileConfig)
if err != nil {
return fileConfig, nil // Return config even if caching fails
}
if dbConfig == nil {
// Create new cache entry
dbConfig = &repo_model.PagesConfig{
RepoID: repo.ID,
Enabled: fileConfig.Enabled,
Template: repo_model.PagesTemplate(fileConfig.Template),
ConfigJSON: string(configJSON),
ConfigHash: fileHash,
}
if err := repo_model.CreatePagesConfig(ctx, dbConfig); err != nil {
// Log but don't fail
return fileConfig, nil
}
} else {
// Update existing cache
dbConfig.Enabled = fileConfig.Enabled
dbConfig.Template = repo_model.PagesTemplate(fileConfig.Template)
dbConfig.ConfigJSON = string(configJSON)
dbConfig.ConfigHash = fileHash
if err := repo_model.UpdatePagesConfig(ctx, dbConfig); err != nil {
// Log but don't fail
return fileConfig, nil
}
}
return fileConfig, nil
}
// loadConfigFromRepo loads the landing.yaml configuration from the repository
// getDefaultConfig returns a default landing page configuration
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
if template == "" {
template = "simple"
}
return &pages_module.LandingConfig{
Enabled: true,
Template: template,
Hero: pages_module.HeroConfig{
Title: repo.Name,
Tagline: repo.Description,
},
Branding: pages_module.BrandingConfig{
PrimaryColor: "#4183c4",
},
}
}
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return nil, "", err
}
defer gitRepo.Close()
// Try to get the default branch
branch := repo.DefaultBranch
if branch == "" {
branch = "main"
}
// Try to get the commit
commit, err := gitRepo.GetBranchCommit(branch)
if err != nil {
return nil, "", err
}
// Try to get the config file
var content []byte
entry, err := commit.GetTreeEntryByPath(LandingConfigPath)
if err != nil {
// Try alternative path
entry, err = commit.GetTreeEntryByPath(LandingConfigPathAlt)
if err != nil {
return nil, "", errors.New("landing config not found")
}
}
reader, err := entry.Blob().DataAsync()
if err != nil {
return nil, "", err
}
defer reader.Close()
content = make([]byte, entry.Blob().Size())
_, err = reader.Read(content)
if err != nil && err.Error() != "EOF" {
return nil, "", err
}
// Parse the config
config, err := pages_module.ParseLandingConfig(content)
if err != nil {
return nil, "", err
}
// Calculate hash for cache invalidation
hash := pages_module.HashConfig(content)
return config, hash, nil
}
// IsPagesEnabled checks if pages is enabled for a repository
func IsPagesEnabled(ctx context.Context, repo *repo_model.Repository) (bool, error) {
config, err := GetPagesConfig(ctx, repo)
if err != nil {
return false, nil // Not enabled if config doesn't exist
}
return config.Enabled, nil
}
// EnablePages enables pages for a repository with default config
func EnablePages(ctx context.Context, repo *repo_model.Repository, template string) error {
if !pages_module.IsValidTemplate(template) {
template = "simple"
}
return repo_model.EnablePages(ctx, repo.ID, repo_model.PagesTemplate(template))
}
// DisablePages disables pages for a repository
func DisablePages(ctx context.Context, repo *repo_model.Repository) error {
return repo_model.DisablePages(ctx, repo.ID)
}
// GetPagesSubdomain returns the subdomain for a repository's pages
func GetPagesSubdomain(repo *repo_model.Repository) string {
// Format: {repo}-{owner}.{domain}
return fmt.Sprintf("%s-%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
}
// GetPagesURL returns the full URL for a repository's pages
func GetPagesURL(repo *repo_model.Repository) string {
subdomain := GetPagesSubdomain(repo)
// This should be configurable
// Extract domain from settings
domain := setting.Domain
return fmt.Sprintf("https://%s.%s", subdomain, domain)
}
// GetPagesDomains returns all custom domains for a repository's pages
func GetPagesDomains(ctx context.Context, repoID int64) ([]*repo_model.PagesDomain, error) {
return repo_model.GetPagesDomainsByRepoID(ctx, repoID)
}
// AddPagesDomain adds a custom domain for pages
func AddPagesDomain(ctx context.Context, repoID int64, domain string, sslExternal bool) (*repo_model.PagesDomain, error) {
// Normalize domain
domain = strings.ToLower(strings.TrimSpace(domain))
// Check if domain already exists
existing, err := repo_model.GetPagesDomainByDomain(ctx, domain)
if err == nil && existing != nil {
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
}
sslStatus := repo_model.SSLStatusPending
if sslExternal {
sslStatus = repo_model.SSLStatusActive
}
pagesDomain := &repo_model.PagesDomain{
RepoID: repoID,
Domain: domain,
SSLStatus: sslStatus,
}
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {
return nil, err
}
return pagesDomain, nil
}
// RemovePagesDomain removes a custom domain
func RemovePagesDomain(ctx context.Context, repoID, domainID int64) error {
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
if err != nil {
return err
}
// Verify domain belongs to this repo
if domain.RepoID != repoID {
return repo_model.ErrPagesDomainNotExist{ID: domainID}
}
return repo_model.DeletePagesDomain(ctx, domainID)
}
// VerifyDomain verifies a custom domain by checking DNS records
func VerifyDomain(ctx context.Context, domainID int64) error {
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
if err != nil {
return err
}
// TODO: Implement actual DNS verification
// For now, just mark as verified
return repo_model.VerifyPagesDomain(ctx, domain.ID)
}
// GetRepoByPagesDomain returns the repository for a pages domain
func GetRepoByPagesDomain(ctx context.Context, domainName string) (*repo_model.Repository, error) {
domain, err := repo_model.GetPagesDomainByDomain(ctx, domainName)
if err != nil {
return nil, err
}
if !domain.Verified {
return nil, errors.New("domain not verified")
}
return repo_model.GetRepositoryByID(ctx, domain.RepoID)
}
// HasPublicLanding checks if a repository has public landing enabled
// This allows private repos to have a public-facing landing page
func HasPublicLanding(ctx context.Context, repo *repo_model.Repository) bool {
config, err := GetPagesConfig(ctx, repo)
if err != nil {
return false
}
return config.Enabled && config.PublicLanding
}
// HasPublicReleases checks if a repository has public releases enabled
// This allows private repos to have publicly accessible releases
func HasPublicReleases(ctx context.Context, repo *repo_model.Repository) bool {
config, err := GetPagesConfig(ctx, repo)
if err != nil {
return false
}
return config.Enabled && config.Advanced.PublicReleases
}