// Package feedback is the golem15.feedback plugin: the embeddable feedback // widget of SummerCMS applications. It ports the WinterCMS Golem15.Feedback // plugin: the widget's public config and submit routes behind a widget key // and an Origin allow-list, the per-user "hide the widget" preference, the // settings screen, and the job that files every submission as a G15Office // task with its screenshot. package feedback import ( "context" "embed" "errors" "io/fs" "net/http" "time" "git.golem15.com/golem15/sm-feedback-plugin/console" "git.golem15.com/golem15/sm-feedback-plugin/models" "git.golem15.com/golem15/sm-feedback-plugin/updates" userclasses "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/surf" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) var ( _ party.Plugin = (*Plugin)(nil) _ pact.HasConfig = (*Plugin)(nil) _ pact.HasLang = (*Plugin)(nil) _ pact.HasMigrations = (*Plugin)(nil) _ pact.HasModels = (*Plugin)(nil) _ pact.HasRoutes = (*Plugin)(nil) _ pact.HasCommands = (*Plugin)(nil) _ surf.BucketProvider = (*Plugin)(nil) ) //go:embed config var configFS embed.FS //go:embed lang var langFS embed.FS // Bucket names of the widget routes (routes.php RateLimiter::for). const ( BucketConfig = "feedback-config" BucketSubmit = "feedback-submit" ) // Plugin is the golem15.feedback plugin. type Plugin struct { app *backpack.App } // ID is the plugin ID, also its config and lang namespace. func (p *Plugin) ID() string { return "golem15.feedback" } // Requires is golem15.user: the preference route is JWT-guarded and the // preference rides on the user payload. func (p *Plugin) Requires() []string { return []string{"golem15.user"} } // Register keeps the application. func (p *Plugin) Register(app *backpack.App) error { p.app = app return nil } // Boot keeps the application and adds feedback_widget_hidden to every // golem15.user payload (fetch, login, update...): the user's preference, // false without a row. The user plugin itself is never changed. func (p *Plugin) Boot(app *backpack.App) error { p.app = app if app != nil && app.Events != nil { app.Events.Listen[*userclasses.GetApiArrayEvent]("golem15.feedback", func(ctx context.Context, e *userclasses.GetApiArrayEvent) error { if e == nil || e.User == nil { return nil } gdb, ok := app.Lookup[*gorm.DB]() if !ok || gdb == nil { return errors.New("feedback: user payload listener has no database") } hidden, err := models.IsWidgetHidden(ctx, gdb, e.User.ID) if err != nil { return err } e.Collected()["feedback_widget_hidden"] = hidden return nil }) } return nil } // ConfigFS ships config/config.yaml, merged under golem15.feedback.*. func (p *Plugin) ConfigFS() fs.FS { return configFS } // LangFS ships the golem15.feedback lang namespace (en, pl). func (p *Plugin) LangFS() fs.FS { return langFS } // Migrations creates the submissions, preferences and settings tables. func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() } // Models is Submission, UserPreference and Settings. func (p *Plugin) Models() []any { return models.All() } // Commands is feedback:import-settings. func (p *Plugin) Commands() []bonfire.Command { return []bonfire.Command{console.ImportSettings(p.app)} } // Buckets are the widget routes' limits by client IP (behind the trusted // proxies): 60 config reads and 10 submissions a minute. func (p *Plugin) Buckets() map[string]surf.Bucket { trusted := surf.TrustedProxies(nil) if p.app != nil { trusted = surf.TrustedProxies(p.app.Config) } byIP := func(r *http.Request) string { return surf.ClientIP(r, trusted) } return map[string]surf.Bucket{ BucketConfig: {Max: 60, Decay: time.Minute, Key: byIP}, BucketSubmit: {Max: 10, Decay: time.Minute, Key: byIP}, } } func init() { party.Register(&Plugin{}) }