gitea/services/pages/pages.go
logikonline b816ee4eec feat: add Phases 3-5 enhancements (org profiles, pages, wiki v2 API)
Phase 3: Organization Public Profile Page
- Pinned repositories with groups
- Public members display with roles
- API endpoints for pinned repos and groups

Phase 4: Gitea Pages Foundation
- Landing page templates (simple, docs, product, portfolio)
- Custom domain support with verification
- YAML configuration parser (.gitea/landing.yaml)
- Repository settings UI for pages

Phase 5: Enhanced Wiki System with V2 API
- Full CRUD operations via v2 API
- Full-text search with WikiIndex table
- Link graph visualization
- Wiki health metrics (orphaned, dead links, outdated)
- Designed for external AI plugin integration
- Developer guide for .NET integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:14:27 -05:00

254 lines
7.3 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pages
import (
"context"
"encoding/json"
"fmt"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
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
}
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
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, "", fmt.Errorf("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}.pages.{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
pagesDomain := setting.AppURL // TODO: Add proper pages domain setting
return fmt.Sprintf("https://%s.pages.%s", subdomain, pagesDomain)
}
// 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) (*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}
}
pagesDomain := &repo_model.PagesDomain{
RepoID: repoID,
Domain: domain,
}
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 int64, 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, fmt.Errorf("domain not verified")
}
return repo_model.GetRepositoryByID(ctx, domain.RepoID)
}