Plugin assets, external scripts and CSP overrides (#4260)

* Add assets for plugins
* Move plugin javascript and css into separate endpoints
* Allow loading external scripts
* Add csp overrides
* Only include enabled plugins
* Move URLMap to utils
* Use URLMap for assets
* Add documentation
This commit is contained in:
WithoutPants
2023-11-19 10:41:16 +11:00
committed by GitHub
parent 4dd4c3c658
commit 222475df82
20 changed files with 621 additions and 105 deletions

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/stashapp/stash/pkg/utils"
"gopkg.in/yaml.v2"
)
@@ -64,27 +65,81 @@ type Config struct {
Settings map[string]SettingConfig `yaml:"settings"`
}
type PluginCSP struct {
ScriptSrc []string `json:"script-src" yaml:"script-src"`
StyleSrc []string `json:"style-src" yaml:"style-src"`
ConnectSrc []string `json:"connect-src" yaml:"connect-src"`
}
type UIConfig struct {
// Content Security Policy configuration for the plugin.
CSP PluginCSP `yaml:"csp"`
// Javascript files that will be injected into the stash UI.
// These may be URLs or paths to files relative to the plugin configuration file.
Javascript []string `yaml:"javascript"`
// CSS files that will be injected into the stash UI.
// These may be URLs or paths to files relative to the plugin configuration file.
CSS []string `yaml:"css"`
// Assets is a map of URL prefixes to hosted directories.
// This allows plugins to serve static assets from a URL path.
// Plugin assets are exposed via the /plugin/{pluginId}/assets path.
// For example, if the plugin configuration file contains:
// /foo: bar
// /bar: baz
// /: root
// Then the following requests will be mapped to the following files:
// /plugin/{pluginId}/assets/foo/file.txt -> {pluginDir}/foo/file.txt
// /plugin/{pluginId}/assets/bar/file.txt -> {pluginDir}/baz/file.txt
// /plugin/{pluginId}/assets/file.txt -> {pluginDir}/root/file.txt
Assets utils.URLMap `yaml:"assets"`
}
func isURL(s string) bool {
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
}
func (c UIConfig) getCSSFiles(parent Config) []string {
ret := make([]string, len(c.CSS))
for i, v := range c.CSS {
ret[i] = filepath.Join(parent.getConfigPath(), v)
var ret []string
for _, v := range c.CSS {
if !isURL(v) {
ret = append(ret, filepath.Join(parent.getConfigPath(), v))
}
}
return ret
}
func (c UIConfig) getExternalCSS() []string {
var ret []string
for _, v := range c.CSS {
if isURL(v) {
ret = append(ret, v)
}
}
return ret
}
func (c UIConfig) getJavascriptFiles(parent Config) []string {
ret := make([]string, len(c.Javascript))
for i, v := range c.Javascript {
ret[i] = filepath.Join(parent.getConfigPath(), v)
var ret []string
for _, v := range c.Javascript {
if !isURL(v) {
ret = append(ret, filepath.Join(parent.getConfigPath(), v))
}
}
return ret
}
func (c UIConfig) getExternalScripts() []string {
var ret []string
for _, v := range c.Javascript {
if isURL(v) {
ret = append(ret, v)
}
}
return ret
@@ -184,10 +239,14 @@ func (c Config) toPlugin() *Plugin {
Tasks: c.getPluginTasks(false),
Hooks: c.getPluginHooks(false),
UI: PluginUI{
Javascript: c.UI.getJavascriptFiles(c),
CSS: c.UI.getCSSFiles(c),
ExternalScript: c.UI.getExternalScripts(),
ExternalCSS: c.UI.getExternalCSS(),
Javascript: c.UI.getJavascriptFiles(c),
CSS: c.UI.getCSSFiles(c),
Assets: c.UI.Assets,
},
Settings: c.getPluginSettings(),
Settings: c.getPluginSettings(),
ConfigPath: c.path,
}
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/stashapp/stash/pkg/session"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
type Plugin struct {
@@ -35,14 +36,39 @@ type Plugin struct {
Settings []PluginSetting `json:"settings"`
Enabled bool `json:"enabled"`
// ConfigPath is the path to the plugin's configuration file.
ConfigPath string `json:"-"`
}
type PluginUI struct {
// Content Security Policy configuration for the plugin.
CSP PluginCSP `json:"csp"`
// External Javascript files that will be injected into the stash UI.
ExternalScript []string `json:"external_script"`
// External CSS files that will be injected into the stash UI.
ExternalCSS []string `json:"external_css"`
// Javascript files that will be injected into the stash UI.
Javascript []string `json:"javascript"`
// CSS files that will be injected into the stash UI.
CSS []string `json:"css"`
// Assets is a map of URL prefixes to hosted directories.
// This allows plugins to serve static assets from a URL path.
// Plugin assets are exposed via the /plugin/{pluginId}/assets path.
// For example, if the plugin configuration file contains:
// /foo: bar
// /bar: baz
// /: root
// Then the following requests will be mapped to the following files:
// /plugin/{pluginId}/assets/foo/file.txt -> {pluginDir}/foo/file.txt
// /plugin/{pluginId}/assets/bar/file.txt -> {pluginDir}/baz/file.txt
// /plugin/{pluginId}/assets/file.txt -> {pluginDir}/root/file.txt
Assets utils.URLMap `json:"assets"`
}
type PluginSetting struct {
@@ -173,6 +199,22 @@ func (c Cache) ListPlugins() []*Plugin {
return ret
}
// GetPlugin returns the plugin with the given ID.
// Returns nil if the plugin is not found.
func (c Cache) GetPlugin(id string) *Plugin {
disabledPlugins := c.config.GetDisabledPlugins()
plugin := c.getPlugin(id)
if plugin != nil {
p := plugin.toPlugin()
disabled := sliceutil.Contains(disabledPlugins, p.ID)
p.Enabled = !disabled
return p
}
return nil
}
// ListPluginTasks returns all runnable plugin tasks in all loaded plugins.
func (c Cache) ListPluginTasks() []*PluginTask {
var ret []*PluginTask

30
pkg/utils/urlmap.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import "strings"
// URLMap is a map of URL prefixes to filesystem locations
type URLMap map[string]string
// GetFilesystemLocation returns the adjusted URL and the filesystem location
func (m URLMap) GetFilesystemLocation(url string) (newURL string, fsPath string) {
newURL = url
if m == nil {
return
}
root := m["/"]
for k, v := range m {
if k != "/" && strings.HasPrefix(url, k) {
newURL = strings.TrimPrefix(url, k)
fsPath = v
return
}
}
if root != "" {
fsPath = root
return
}
return
}

70
pkg/utils/urlmap_test.go Normal file
View File

@@ -0,0 +1,70 @@
package utils
import (
"testing"
)
func TestURLMap_GetFilesystemLocation(t *testing.T) {
// create the URLMap
urlMap := make(URLMap)
urlMap["/"] = "root"
urlMap["/foo"] = "bar"
empty := make(URLMap)
var nilMap URLMap
tests := []struct {
name string
urlMap URLMap
url string
wantNewURL string
wantFsPath string
}{
{
name: "simple",
urlMap: urlMap,
url: "/foo/bar",
wantNewURL: "/bar",
wantFsPath: "bar",
},
{
name: "root",
urlMap: urlMap,
url: "/baz",
wantNewURL: "/baz",
wantFsPath: "root",
},
{
name: "root",
urlMap: urlMap,
url: "/baz",
wantNewURL: "/baz",
wantFsPath: "root",
},
{
name: "empty",
urlMap: empty,
url: "/xyz",
wantNewURL: "/xyz",
wantFsPath: "",
},
{
name: "nil",
urlMap: nilMap,
url: "/xyz",
wantNewURL: "/xyz",
wantFsPath: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNewURL, gotFsPath := tt.urlMap.GetFilesystemLocation(tt.url)
if gotNewURL != tt.wantNewURL {
t.Errorf("URLMap.GetFilesystemLocation() gotNewURL = %v, want %v", gotNewURL, tt.wantNewURL)
}
if gotFsPath != tt.wantFsPath {
t.Errorf("URLMap.GetFilesystemLocation() gotFsPath = %v, want %v", gotFsPath, tt.wantFsPath)
}
})
}
}