Files
sm-feedback-plugin/console/import_settings.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

181 lines
5.3 KiB
Go

// Package console holds the golem15.feedback operator commands.
package console
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"git.golem15.com/golem15/sm-feedback-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/lagoon"
"gorm.io/gorm"
)
// ErrSettingsCustomised refuses an import over settings an operator has
// already changed (the row was saved after it was created).
var ErrSettingsCustomised = errors.New("golem15_feedback_settings was already changed; nothing was imported (pass --force to overwrite it)")
// importNotice is a clean stop: there is nothing to import.
type importNotice struct{ msg string }
func (n *importNotice) Error() string { return n.msg }
// IsNothingToImport reports an import that stopped because the source
// holds nothing (no system_settings table or no golem15_feedback_settings
// row).
func IsNothingToImport(err error) bool {
var n *importNotice
return errors.As(err, &n)
}
// ImportSettings is feedback:import-settings: a one-time copy of the
// WinterCMS widget settings (the system_settings row
// golem15_feedback_settings) into the golem15_feedback_settings singleton,
// including the widget key, so embed snippets keep working. It writes only
// while the singleton still holds what it was created with (or does not
// exist yet); --force overwrites changed settings.
func ImportSettings(app *backpack.App) bonfire.Command {
return bonfire.Command{
Name: "feedback:import-settings",
Description: "Import the WinterCMS golem15_feedback_settings row into golem15_feedback_settings, once.",
Flags: []bonfire.Flag{{
Name: "force",
Description: "Overwrite settings that were already changed",
Bare: true,
}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
if app == nil {
return errors.New("feedback:import-settings: app is nil")
}
db, ok := app.Lookup[*gorm.DB]()
if !ok || db == nil {
return errors.New("feedback:import-settings: database is not configured")
}
force, _ := in.Flag("force")
err := Import(ctx, db, force == "true")
var notice *importNotice
switch {
case errors.As(err, &notice):
out.Info(notice.msg)
return nil
case err != nil:
out.Error(err.Error())
return err
}
out.Success("Imported the feedback widget settings.")
return nil
},
}
}
// Import copies the system_settings row into the singleton. Keys the row
// does not hold keep their current (default) values.
func Import(ctx context.Context, db *gorm.DB, force bool) error {
raw, err := settingsValue(ctx, db)
if err != nil {
return err
}
var values map[string]any
dec := json.NewDecoder(strings.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&values); err != nil {
return fmt.Errorf("feedback:import-settings: the settings are not a JSON object: %w", err)
}
return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
s, err := models.Instance(ctx, tx)
if err != nil {
return err
}
if !force && !s.UpdatedAt.Equal(s.CreatedAt) {
return ErrSettingsCustomised
}
apply(s, values)
s.UpdatedAt = time.Now()
return tx.WithContext(ctx).Save(s).Error
})
}
func settingsValue(ctx context.Context, db *gorm.DB) (string, error) {
if !db.WithContext(ctx).Migrator().HasTable("system_settings") {
return "", &importNotice{msg: "No system_settings table: nothing to import."}
}
var values []string
if err := db.WithContext(ctx).Raw(`SELECT value FROM system_settings WHERE item = ?`, models.SettingsCode).Scan(&values).Error; err != nil {
return "", err
}
if len(values) == 0 {
return "", &importNotice{msg: "No " + models.SettingsCode + " row in system_settings: nothing to import."}
}
return values[0], nil
}
func apply(s *models.Settings, v map[string]any) {
if x, ok := v["enabled"]; ok {
s.Enabled = truthy(x)
}
if x, ok := v["allow_hide"]; ok {
s.AllowHide = truthy(x)
}
if key := str(v["widget_key"]); key != nil && strings.TrimSpace(*key) != "" {
s.WidgetKey = *key
}
for name, field := range map[string]**string{
"position": &s.Position,
"allowed_origins": &s.AllowedOrigins,
"color_primary": &s.ColorPrimary,
"color_accent": &s.ColorAccent,
"label_title_pl": &s.LabelTitlePL,
"label_title_en": &s.LabelTitleEN,
"label_placeholder_pl": &s.LabelPlaceholderPL,
"label_placeholder_en": &s.LabelPlaceholderEN,
"label_success_pl": &s.LabelSuccessPL,
"label_success_en": &s.LabelSuccessEN,
"g15office_task_status": &s.G15OfficeTaskStatus,
"g15office_task_priority": &s.G15OfficeTaskPriority,
} {
if x, ok := v[name]; ok {
*field = str(x)
}
}
}
// str is a stored scalar as a string; null stays nil.
func str(v any) *string {
var s string
switch t := v.(type) {
case nil:
return nil
case string:
s = t
case json.Number:
s = t.String()
case bool:
// PHP's string of a boolean.
if t {
s = "1"
}
default:
return nil
}
return &s
}
// truthy is PHP's (bool) of a stored switch value.
func truthy(v any) bool {
switch t := v.(type) {
case bool:
return t
case string:
return t != "" && t != "0"
case json.Number:
f, err := t.Float64()
return err != nil || f != 0
}
return false
}