Files
sm-feedback-plugin/plugin.go
Jakub Zych af5d77c997 feat: G15Office job, widget settings and submissions admin, embed.js and the settings importer
- SyncG15Office files each submission as a G15Office task with its screenshot through a fetchguard client limited to the configured host; a River job with 3 attempts, PHP's messages on failure, nothing sent when unconfigured
- the replay of PHP's recorded G15Office exchange (classes/testdata) asserts the task JSON, headers and the screenshot part
- settings screen (cabana field types only), read-only submissions list, manage_settings permission and navigation
- embed.js served byte for byte at /plugins/golem15/feedback/assets/js/embed.js
- feedback:import-settings copies the WinterCMS settings row once
2026-10-04 00:22:08 +02:00

130 lines
4.0 KiB
Go

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