mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
30
pkg/utils/urlmap.go
Normal 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
70
pkg/utils/urlmap_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user