The public screenshot kept the client file's extension, so a valid image named x.html or x.svg was stored and served under that type. The disk name now carries the client extension only when it names the sniffed type, else the type's own; file_name keeps only the client's base name.
golem15.feedback
The shared feedback-widget plugin for SummerCMS applications: an embeddable widget's public config and submit API behind a widget key and an Origin allow-list, a per-user "hide the widget" preference, the widget settings, and a job that files every submission as a G15Office task.
import _ "git.golem15.com/golem15/sm-feedback-plugin"
Module git.golem15.com/golem15/sm-feedback-plugin, package feedback, plugin ID golem15.feedback.
Overview
The plugin is a compiled SummerCMS plugin ported from the WinterCMS Golem15.Feedback plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go. It requires golem15.user.
The widget routes are public. The widget key in the URL is a public identifier (it ships in the embed snippet), not a secret: the real gate is the Origin check against the operator's allowed-origins list, backed by per-IP rate limits.
The plugin ID, the table names, the config keys and the settings code are a contract shared by every application that mounts the plugin: change them only in a backwards-compatible way.
Features
- Widget config.
GET /_feedback/api/v1/{key}/configanswers the widget's position, whether visitors may hide it, its colors and its labels (?lang=enselects the English labels, anything else the Polish ones). A disabled widget, an empty key or a key that differs (compared in constant time) is 404{"error":true,"message":"Not found"}. - Origin gate. A request without an
Originheader passes. AnOriginpasses only when its URL host, lower-cased, is on the allowed-origins list (one entry per line; a line is reduced to its URL host when it has one). An empty list refuses everyOriginwith 403{"error":true,"message":"Origin not allowed"}, so a widget that was never configured cannot be embedded anywhere. - Submissions.
POST /_feedback/api/v1/{key}/submit(multipart, from the widget) passes the same key and Origin gate, then validates the report as the WinterCMS plugin does:message(required, up to 5000 characters),type(bug,featureorother),email(optional, up to 255),page_url(required, up to 2000),user_agent(up to 500),console_log(up to 20000) andscreenshot(an optional JPEG, PNG, GIF or WebP of up to 10240 KB). A failure is 422{"error":true,"message":"Validation failed","details":{...}}. A screenshot whose bytes do not decode as one of those images (classes.IsAllowedImage) is 422 withdetails.screenshot = ["The file is not a valid image."]. The submission (statuspending), its public screenshot (attachOnescreenshot) and its G15Office job are written in one transaction, and the answer is 202{"success":true}: the visitor never waits on G15Office. - Hiding the widget.
PUT /_feedback/api/v1/me/hidden(behind golem15.user'sjwt.auth) stores the signed-in user's preference:hiddenmust be a Laravel boolean (true,false,1,0,"1","0"), else 422{"error":"Validation failed","errors":{...}}. The answer is{"hidden":bool}. Every golem15.user payload (fetch, login, update) carriesfeedback_widget_hiddenthrough aGetApiArrayEventlistener: the stored value, false without a row. - CORS. The widget's preflights are answered by the framework's path-scoped CORS layer, so the plugin registers no
OPTIONSroute. - Rate limits. The named buckets
feedback-config(60 a minute) andfeedback-submit(10 a minute) count per client IP behind the configured trusted proxies. - Settings.
models.Settingsis the singleton row ofgolem15_feedback_settings.models.Instancecreates it on first access with the WinterCMS defaults and a new widget key (wk_plus 32 random letters and digits). Operators edit it under Settings › Feedback Widget (codefeedback, permissiongolem15.feedback.manage_settings): the WinterCMS form with cabana's field types, the colors as text fields and the widget key read-only. A row saved without a key gets a new one. - G15Office tasks. Every submission queues a
classes.SyncG15OfficeArgsjob (kindgolem15.feedback.sync_g15office, queuefeedback, 3 attempts, asummer_jobsrow labelled with the WinterCMS job class).classes.SyncG15Officefiles it withclasses.G15OfficeClient:POST <base>/_support/api/v1/projects/<project>/taskswith the title (Feedback:and the message collapsed and cut to 80 columns), a Markdown description (the message, the page, the reporter email and user agent, the console log cut to 4000 columns), the task type (bug→ Bug,feature→ Feature,other→ Task), the configured task status and priority; thenPOST <base>/_support/api/v1/tasks/<id>/attachmentswith the screenshot as the multipart fieldfile. Requests carryAuthorization: Bearer <token>andAccept: application/jsonand go through afetchguard.Clientlimited to the base URL's host (https only). Success marks the submissionsentwith the task id; a failure marks itfailedwith the error and the job is retried. Without a base URL, token and project nothing is sent: the job fails with the WinterCMS "not configured" message. - Submissions list. Admin › Feedback › Submissions (controller
golem15.feedback.submissions) is a read-only list: type, status, reporter email, page, message, task id and date, all as plain text. - Widget script.
GET /plugins/golem15/feedback/assets/js/embed.jsserves the WinterCMS plugin'sembed.jsbyte for byte (application/javascript, with an ETag), at the path embedding pages already use. - Settings import.
feedback:import-settingscopies a WinterCMS installation's widget settings, the widget key included, into the singleton once.
Usage
Mount the plugin in an application as a git submodule at plugins/golem15/feedback:
git submodule add git@git.golem15.com:golem15/sm-feedback-plugin.git plugins/golem15/feedback
Add it to the application's go.work and go.mod:
use ./plugins/golem15/feedback
require git.golem15.com/golem15/sm-feedback-plugin v0.0.0
replace git.golem15.com/golem15/sm-feedback-plugin => ./plugins/golem15/feedback
List it in summer.yaml after golem15.user and run summer build:
module: git.golem15.com/golem15/acme
binary: acme
plugins:
- id: golem15.user
module: git.golem15.com/golem15/sm-user-plugin
- id: golem15.feedback
module: git.golem15.com/golem15/sm-feedback-plugin
The application's http.cors.paths must list _feedback/api/*: the widget calls the API from other origins. A page embeds the widget with its key:
<script src="https://backend.example.com/plugins/golem15/feedback/assets/js/embed.js" data-key="wk_..." data-lang="en" defer></script>
Set the G15Office connection with the three SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__* variables (see Configuration) and run a queue worker: serve runs one in-process when queue.work_in_serve is on, queue:work in its own process. After moving an installation from WinterCMS, run feedback:import-settings once so embed snippets keep their widget key.
The plugin's own go.mod replaces the framework with ../../../../summercms.go and the user plugin with ../user, so it expects the framework checkout next to the application checkout and the user plugin mounted beside it.
API reference
feedback:Plugin, thegolem15.feedbackplugin (config, lang, migrations, models, routes, buckets, jobs, commands, admin controllers, permissions, navigation, settings);BucketConfig,BucketSubmit,EmbedJSPath,EmbedJS,SettingsCode.controllers/api:Config,Submit,SubmitRules,MeHidden,MeHiddenRules,KeyMatches,OriginAllowed.classes:IsAllowedImage,SniffImageMIME,AllowedImageMIMEs;Screenshot,StoreSubmission,ScreenshotOf; the job contractSyncG15OfficeArgs,SyncG15OfficeKind,SyncG15OfficeQueue,SyncG15OfficeLabel,SyncG15OfficeAttempts; the jobSyncG15Office,SyncDeps,TaskTitle,TaskDescription,StrLimit,StrWidth; the clientG15OfficeClient,NewG15OfficeClient,NewG15OfficeClientWith,TaskFields,ErrNotConfigured,G15OfficeTimeout,ConfigBaseURL,ConfigToken,ConfigProject.controllers:SubmissionsAdminController,AdminControllers,PermissionManageSettings.console:ImportSettings,Import,IsNothingToImport,ErrSettingsCustomised.models:Settings(tablegolem15_feedback_settings),SettingsID,SettingsCode,DefaultSettings,Instance,NewWidgetKey,Labels,URLHost,ASCIILower;Submission(tablegolem15_feedback_submissions),StatusPending,StatusSent,StatusFailed,TypeBug,TypeFeature,TypeOther,ScreenshotField,G15OfficeTypeMap;UserPreference(tablegolem15_feedback_user_preferences),IsWidgetHidden,SetWidgetHidden;Register,All.updates: the migrations (202610030201_create_feedback_submissions,202610030202_create_feedback_user_preferences,202610030203_create_feedback_settings),Register,All.
Configuration
Defaults live in config/config.yaml and are merged under golem15.feedback.*.
| Key | Default | Meaning |
|---|---|---|
golem15.feedback.g15_office.base_url |
empty | The G15Office base URL submissions are filed under (https; requests may reach only its host). |
golem15.feedback.g15_office.token |
empty | The G15Office API token. Never commit it. |
golem15.feedback.g15_office.project |
empty | The G15Office project the tasks are created in. |
Override them with SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL, SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__TOKEN and SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__PROJECT.
CLI commands
| Command | Purpose |
|---|---|
feedback:import-settings [--force] |
Copy the WinterCMS system_settings row golem15_feedback_settings (widget key, origins, colors, labels, G15Office status and priority) into golem15_feedback_settings. It refuses settings that were already changed unless --force is given, and reports a missing system_settings table or row without failing. |
Dependencies
- The SummerCMS framework,
git.golem15.com/golem15/summercms(modulesbackpack,bonfire,bouncer,cabana,compass,conga,fetchguard,lagoon,pact,party,phrasebook,surf,wire;tidein tests). - gocloud.dev blob storage for the screenshots, through the application's uploads bucket.
- The user plugin,
git.golem15.com/golem15/sm-user-plugin. - GORM and gormigrate, as pinned in
go.mod. - testcontainers-go for the Postgres-backed tests.
Testing
go test ./... # starts Postgres with testcontainers (needs Docker)
classes/testdata/g15office-task.upstream.yaml is the G15Office exchange the WinterCMS job made, recorded through the framework's upstream recording proxy; TestSyncG15Office replays it offline and fails on any difference in the requests (URL, headers, the task JSON, the screenshot part).