| /* | |
| * 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) | |
| } |