// Copyright 2026 MarketAlly. All rights reserved. // SPDX-License-Identifier: MIT package plugins import ( "context" "sync" "code.gitcaddy.com/server/v3/modules/log" "xorm.io/xorm" ) var ( registry = make(map[string]Plugin) registryLock sync.RWMutex ) // Register adds a plugin to the registry func Register(p Plugin) { registryLock.Lock() defer registryLock.Unlock() name := p.Name() if _, exists := registry[name]; exists { log.Warn("Plugin %s is already registered, skipping", name) return } registry[name] = p log.Info("Plugin registered: %s v%s", name, p.Version()) } // Get returns a plugin by name func Get(name string) Plugin { registryLock.RLock() defer registryLock.RUnlock() return registry[name] } // All returns all registered plugins func All() []Plugin { registryLock.RLock() defer registryLock.RUnlock() plugins := make([]Plugin, 0, len(registry)) for _, p := range registry { plugins = append(plugins, p) } return plugins } // InitAll initializes all registered plugins func InitAll(ctx context.Context) error { registryLock.RLock() defer registryLock.RUnlock() for name, p := range registry { log.Info("Initializing plugin: %s", name) // Check license for licensed plugins if lp, ok := p.(LicensedPlugin); ok { if err := lp.ValidateLicense(ctx); err != nil { log.Warn("Plugin %s license validation failed: %v", name, err) // Continue but features may be limited } else { info := lp.LicenseInfo() if info != nil && info.Valid { log.Info("Plugin %s licensed: tier=%s", name, info.Tier) } } } if err := p.Init(ctx); err != nil { log.Error("Failed to initialize plugin %s: %v", name, err) return err } } return nil } // ShutdownAll shuts down all registered plugins func ShutdownAll(ctx context.Context) { registryLock.RLock() defer registryLock.RUnlock() for name, p := range registry { log.Info("Shutting down plugin: %s", name) if err := p.Shutdown(ctx); err != nil { log.Error("Failed to shutdown plugin %s: %v", name, err) } } } // RegisterModels registers database models from all plugins func RegisterModels() []any { registryLock.RLock() defer registryLock.RUnlock() var models []any for _, p := range registry { if dp, ok := p.(DatabasePlugin); ok { models = append(models, dp.RegisterModels()...) } } return models } // MigrateAll runs migrations for all database plugins func MigrateAll(ctx context.Context, x *xorm.Engine) error { registryLock.RLock() defer registryLock.RUnlock() for name, p := range registry { if dp, ok := p.(DatabasePlugin); ok { log.Info("Running migrations for plugin: %s", name) if err := dp.Migrate(ctx, x); err != nil { log.Error("Migration failed for plugin %s: %v", name, err) return err } } } return nil } // RegisterWebRoutes registers web UI routes from all plugins func RegisterWebRoutes(m any) { registryLock.RLock() defer registryLock.RUnlock() for name, p := range registry { if wp, ok := p.(WebRoutesPlugin); ok { log.Debug("Registering web routes for plugin: %s", name) wp.RegisterWebRoutes(m) } } } // RegisterAPIRoutes registers API routes from all plugins func RegisterAPIRoutes(m any) { registryLock.RLock() defer registryLock.RUnlock() for name, p := range registry { if ap, ok := p.(APIRoutesPlugin); ok { log.Debug("Registering API routes for plugin: %s", name) ap.RegisterAPIRoutes(m) } } } // RegisterRepoWebRoutes registers per-repository web routes from all plugins // The router parameter should implement WebRouter (e.g., *web.Router) func RegisterRepoWebRoutes(router WebRouter) { registryLock.RLock() defer registryLock.RUnlock() // Create adapter with empty prefix (routes are relative to current group) adapter := NewWebRouterAdapter(router, "") for name, p := range registry { if rp, ok := p.(RepoRoutesPlugin); ok { log.Debug("Registering repo web routes for plugin: %s", name) rp.RegisterRepoWebRoutes(adapter) } } } // RegisterRepoAPIRoutes registers per-repository API routes from all plugins // The router parameter should implement WebRouter (e.g., *web.Router) func RegisterRepoAPIRoutes(router WebRouter) { registryLock.RLock() defer registryLock.RUnlock() // Create adapter with empty prefix (routes are relative to current group) adapter := NewWebRouterAdapter(router, "") for name, p := range registry { if rp, ok := p.(RepoRoutesPlugin); ok { log.Debug("Registering repo API routes for plugin: %s", name) rp.RegisterRepoAPIRoutes(adapter) } } } // IsLicensed checks if a plugin is properly licensed // For licensed plugins without a valid license, this returns true because // we default to Solo tier (free) when no license is present func IsLicensed(name string) bool { registryLock.RLock() defer registryLock.RUnlock() p, exists := registry[name] if !exists { return false } _, ok := p.(LicensedPlugin) if !ok { return true // Non-licensed plugins are always "licensed" } // Licensed plugins default to Solo tier (free) even without a license file // So they are always considered "licensed" if registered return true } // GetLicenseInfo returns license info for a plugin // If the plugin doesn't have a license set, returns the default Solo license func GetLicenseInfo(name string) *LicenseInfo { registryLock.RLock() defer registryLock.RUnlock() p, exists := registry[name] if !exists { return nil } lp, ok := p.(LicensedPlugin) if !ok { return nil } info := lp.LicenseInfo() if info == nil || !info.Valid { // Default to Solo tier when no valid license return DefaultSoloLicense() } return info } // GetLicenseLimits returns the license limits for a plugin func GetLicenseLimits(name string) *LicenseLimits { info := GetLicenseInfo(name) if info == nil { return nil } return GetLimitsForTier(info.Tier) }