- 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
181 lines
5.3 KiB
Go
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, ¬ice):
|
|
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
|
|
}
|