/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package plugin import ( "encoding/json" "sync" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/i18n" "xorm.io/xorm" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/gin-gonic/gin" ) // Data is defined here to avoid circular dependency with internal/base/data type Data struct { DB *xorm.Engine Cache cache.Cache } // GinContext is a wrapper of gin.Context // We export it to make it easy to use in plugins type GinContext = gin.Context // StatusManager is a manager that manages the status of plugins // Init Plugins: // json.Unmarshal([]byte(`{"plugin1": true, "plugin2": false}`), &plugin.StatusManager) // Dump Status: // json.Marshal(plugin.StatusManager) var StatusManager = statusManager{ status: make(map[string]bool), } // Register registers a plugin func Register(p Base) { registerBase(p) if _, ok := p.(Config); ok { registerConfig(p.(Config)) } if _, ok := p.(UserConfig); ok { registerUserConfig(p.(UserConfig)) } if _, ok := p.(Connector); ok { registerConnector(p.(Connector)) } if _, ok := p.(Parser); ok { registerParser(p.(Parser)) } if _, ok := p.(Filter); ok { registerFilter(p.(Filter)) } if _, ok := p.(Storage); ok { registerStorage(p.(Storage)) } if _, ok := p.(Cache); ok { registerCache(p.(Cache)) } if _, ok := p.(UserCenter); ok { registerUserCenter(p.(UserCenter)) } if _, ok := p.(Agent); ok { registerAgent(p.(Agent)) } if _, ok := p.(Search); ok { registerSearch(p.(Search)) } if _, ok := p.(Notification); ok { registerNotification(p.(Notification)) } if _, ok := p.(Reviewer); ok { registerReviewer(p.(Reviewer)) } if _, ok := p.(Captcha); ok { registerCaptcha(p.(Captcha)) } if _, ok := p.(Embed); ok { registerEmbed(p.(Embed)) } if _, ok := p.(Render); ok { registerRender(p.(Render)) } if _, ok := p.(CDN); ok { registerCDN(p.(CDN)) } if _, ok := p.(Importer); ok { registerImporter(p.(Importer)) } if _, ok := p.(KVStorage); ok { registerKVStorage(p.(KVStorage)) } if _, ok := p.(Sidebar); ok { registerSidebar(p.(Sidebar)) } if _, ok := p.(VectorSearch); ok { registerVectorSearch(p.(VectorSearch)) } } type Stack[T Base] struct { plugins []T } type RegisterFn[T Base] func(p T) type Caller[T Base] func(p T) error type CallFn[T Base] func(fn Caller[T]) error // MakePlugin creates a plugin caller and register stack manager // The parameter super presents if the plugin can be disabled. // It returns a register function and a caller function // The register function is used to register a plugin, it will be called in the plugin's init function // The caller function is used to call all registered plugins func MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) { stack := Stack[T]{} call := func(fn Caller[T]) error { for _, p := range stack.plugins { // If the plugin is disabled, skip it if !super && !StatusManager.IsEnabled(p.Info().SlugName) { continue } if err := fn(p); err != nil { return err } } return nil } register := func(p T) { for _, plugin := range stack.plugins { if plugin.Info().SlugName == p.Info().SlugName { panic("plugin " + p.Info().SlugName + " is already registered") } } stack.plugins = append(stack.plugins, p) } return call, register } type statusManager struct { lock sync.Mutex status map[string]bool } func (m *statusManager) Enable(name string, enabled bool) { m.lock.Lock() defer m.lock.Unlock() if !enabled { m.status[name] = enabled return } m.status[name] = enabled for _, slugName := range coordinatedCaptchaPlugins(name) { m.status[slugName] = false } for _, slugName := range coordinatedCDNPlugins(name) { m.status[slugName] = false } } func (m *statusManager) IsEnabled(name string) bool { if status, ok := m.status[name]; ok { return status } return false } // MarshalJSON implements the json.Marshaler interface. func (m *statusManager) MarshalJSON() ([]byte, error) { return json.Marshal(m.status) } // UnmarshalJSON implements the json.Unmarshaler interface. func (m *statusManager) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.status) } // Translate translates the key to the current language of the context func Translate(ctx *GinContext, key string) string { return translator.Tr(handler.GetLangByCtx(ctx), key) } // TranslateWithData translates the key to the language with data func TranslateWithData(lang i18n.Language, key string, data any) string { return translator.TrWithData(lang, key, data) } // TranslateFn presents a generator of translated string. // We use it to delegate the translation work outside the plugin. type TranslateFn func(ctx *GinContext) string // Translator contains a function that translates the key to the current language of the context type Translator struct { Fn TranslateFn } // MakeTranslator generates a translator from the key func MakeTranslator(key string) Translator { t := func(ctx *GinContext) string { return Translate(ctx, key) } return Translator{Fn: t} } // Translate translates the key to the current language of the context func (t Translator) Translate(ctx *GinContext) string { if t.Fn == nil { return "" } return t.Fn(ctx) }