gitea/models/repo/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

310 lines
9.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
)
// PagesTemplate represents the type of landing page template
type PagesTemplate string
const (
PagesTemplateSimple PagesTemplate = "simple"
PagesTemplateDocumentation PagesTemplate = "documentation"
PagesTemplateProduct PagesTemplate = "product"
PagesTemplatePortfolio PagesTemplate = "portfolio"
)
// SSLStatus represents the SSL certificate status
type SSLStatus string
const (
SSLStatusPending SSLStatus = "pending"
SSLStatusActive SSLStatus = "active"
SSLStatusExpiring SSLStatus = "expiring"
SSLStatusExpired SSLStatus = "expired"
SSLStatusError SSLStatus = "error"
)
func init() {
db.RegisterModel(new(PagesDomain))
db.RegisterModel(new(PagesConfig))
}
// PagesDomain represents a custom domain mapping for Gitea Pages
type PagesDomain struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Domain string `xorm:"UNIQUE NOT NULL"`
Verified bool `xorm:"DEFAULT false"`
VerificationToken string `xorm:"VARCHAR(64)"`
SSLStatus SSLStatus `xorm:"VARCHAR(32) DEFAULT 'pending'"`
SSLCertExpiry timeutil.TimeStamp `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
VerifiedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
Repo *Repository `xorm:"-"`
}
// TableName returns the table name for PagesDomain
func (d *PagesDomain) TableName() string {
return "pages_domain"
}
// PagesConfig represents the cached configuration for a repository's landing page
type PagesConfig struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE NOT NULL"`
Enabled bool `xorm:"DEFAULT false"`
Template PagesTemplate `xorm:"VARCHAR(32) DEFAULT 'simple'"`
ConfigJSON string `xorm:"TEXT"` // Cached parsed config from landing.yaml
ConfigHash string `xorm:"VARCHAR(64)"` // Hash for invalidation
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
Repo *Repository `xorm:"-"`
}
// TableName returns the table name for PagesConfig
func (c *PagesConfig) TableName() string {
return "pages_config"
}
// GenerateVerificationToken generates a random token for domain verification
func GenerateVerificationToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
// GetPagesDomainByID returns a pages domain by ID
func GetPagesDomainByID(ctx context.Context, id int64) (*PagesDomain, error) {
domain := new(PagesDomain)
has, err := db.GetEngine(ctx).ID(id).Get(domain)
if err != nil {
return nil, err
}
if !has {
return nil, ErrPagesDomainNotExist{ID: id}
}
return domain, nil
}
// GetPagesDomainByDomain returns a pages domain by domain name
func GetPagesDomainByDomain(ctx context.Context, domainName string) (*PagesDomain, error) {
domain := new(PagesDomain)
has, err := db.GetEngine(ctx).Where("domain = ?", strings.ToLower(domainName)).Get(domain)
if err != nil {
return nil, err
}
if !has {
return nil, ErrPagesDomainNotExist{Domain: domainName}
}
return domain, nil
}
// GetPagesDomainsByRepoID returns all custom domains for a repository
func GetPagesDomainsByRepoID(ctx context.Context, repoID int64) ([]*PagesDomain, error) {
domains := make([]*PagesDomain, 0, 5)
return domains, db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&domains)
}
// CreatePagesDomain creates a new custom domain for pages
func CreatePagesDomain(ctx context.Context, domain *PagesDomain) error {
// Normalize domain to lowercase
domain.Domain = strings.ToLower(domain.Domain)
// Generate verification token
token, err := GenerateVerificationToken()
if err != nil {
return err
}
domain.VerificationToken = token
domain.SSLStatus = SSLStatusPending
_, err = db.GetEngine(ctx).Insert(domain)
return err
}
// UpdatePagesDomain updates a pages domain
func UpdatePagesDomain(ctx context.Context, domain *PagesDomain) error {
_, err := db.GetEngine(ctx).ID(domain.ID).AllCols().Update(domain)
return err
}
// DeletePagesDomain deletes a pages domain
func DeletePagesDomain(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))
return err
}
// DeletePagesDomainsByRepoID deletes all pages domains for a repository
func DeletePagesDomainsByRepoID(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(PagesDomain))
return err
}
// VerifyPagesDomain marks a domain as verified
func VerifyPagesDomain(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Cols("verified", "verified_unix").Update(&PagesDomain{
Verified: true,
VerifiedUnix: timeutil.TimeStampNow(),
})
return err
}
// GetPagesConfigByRepoID returns the pages config for a repository
func GetPagesConfigByRepoID(ctx context.Context, repoID int64) (*PagesConfig, error) {
config := new(PagesConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(config)
if err != nil {
return nil, err
}
if !has {
return nil, nil // No config means pages not enabled
}
return config, nil
}
// CreatePagesConfig creates a new pages config for a repository
func CreatePagesConfig(ctx context.Context, config *PagesConfig) error {
_, err := db.GetEngine(ctx).Insert(config)
return err
}
// UpdatePagesConfig updates a pages config
func UpdatePagesConfig(ctx context.Context, config *PagesConfig) error {
_, err := db.GetEngine(ctx).ID(config.ID).AllCols().Update(config)
return err
}
// DeletePagesConfig deletes a pages config
func DeletePagesConfig(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(PagesConfig))
return err
}
// EnablePages enables pages for a repository
func EnablePages(ctx context.Context, repoID int64, template PagesTemplate) error {
config, err := GetPagesConfigByRepoID(ctx, repoID)
if err != nil {
return err
}
if config == nil {
// Create new config
config = &PagesConfig{
RepoID: repoID,
Enabled: true,
Template: template,
}
return CreatePagesConfig(ctx, config)
}
// Update existing config
config.Enabled = true
config.Template = template
return UpdatePagesConfig(ctx, config)
}
// DisablePages disables pages for a repository
func DisablePages(ctx context.Context, repoID int64) error {
config, err := GetPagesConfigByRepoID(ctx, repoID)
if err != nil {
return err
}
if config == nil {
return nil // Already disabled
}
config.Enabled = false
return UpdatePagesConfig(ctx, config)
}
// IsPagesEnabled checks if pages is enabled for a repository
func IsPagesEnabled(ctx context.Context, repoID int64) (bool, error) {
config, err := GetPagesConfigByRepoID(ctx, repoID)
if err != nil {
return false, err
}
return config != nil && config.Enabled, nil
}
// ErrPagesDomainNotExist represents a "pages domain not exist" error
type ErrPagesDomainNotExist struct {
ID int64
Domain string
}
func (err ErrPagesDomainNotExist) Error() string {
if err.Domain != "" {
return fmt.Sprintf("pages domain does not exist [domain: %s]", err.Domain)
}
return fmt.Sprintf("pages domain does not exist [id: %d]", err.ID)
}
// IsErrPagesDomainNotExist checks if an error is ErrPagesDomainNotExist
func IsErrPagesDomainNotExist(err error) bool {
_, ok := err.(ErrPagesDomainNotExist)
return ok
}
// ErrPagesDomainAlreadyExist represents a "pages domain already exist" error
type ErrPagesDomainAlreadyExist struct {
Domain string
}
func (err ErrPagesDomainAlreadyExist) Error() string {
return fmt.Sprintf("pages domain already exists [domain: %s]", err.Domain)
}
// IsErrPagesDomainAlreadyExist checks if an error is ErrPagesDomainAlreadyExist
func IsErrPagesDomainAlreadyExist(err error) bool {
_, ok := err.(ErrPagesDomainAlreadyExist)
return ok
}
// ErrPagesConfigNotExist represents a "pages config not exist" error
type ErrPagesConfigNotExist struct {
RepoID int64
}
func (err ErrPagesConfigNotExist) Error() string {
return fmt.Sprintf("pages config does not exist [repo_id: %d]", err.RepoID)
}
// IsErrPagesConfigNotExist checks if an error is ErrPagesConfigNotExist
func IsErrPagesConfigNotExist(err error) bool {
_, ok := err.(ErrPagesConfigNotExist)
return ok
}
// GetPagesConfig returns the pages config for a repository with proper error handling
func GetPagesConfig(ctx context.Context, repoID int64) (*PagesConfig, error) {
config := new(PagesConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(config)
if err != nil {
return nil, err
}
if !has {
return nil, ErrPagesConfigNotExist{RepoID: repoID}
}
return config, nil
}
// GetPagesDomains returns all custom domains for a repository
func GetPagesDomains(ctx context.Context, repoID int64) ([]*PagesDomain, error) {
return GetPagesDomainsByRepoID(ctx, repoID)
}