-
Notifications
You must be signed in to change notification settings - Fork 1
/
asset.go
277 lines (246 loc) · 8.71 KB
/
asset.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
package ambient
import (
"bytes"
"fmt"
"html"
"io"
"io/fs"
"net/http"
"os"
"strings"
)
// AssetLocation is a location where assets can be added.
type AssetLocation string
// AssetType is a type of asset.
type AssetType string
// AuthType is a type of authentication.
type AuthType string
// LayoutType is a type of layout.
type LayoutType string
const (
// LocationHead is at the bottom of the HTML <head> section.
LocationHead AssetLocation = "head"
// LocationHeader is at the top the HTML <header> section.
LocationHeader AssetLocation = "header"
// LocationMain is at the bottom of the HTML <main> section.
LocationMain AssetLocation = "main"
// LocationFooter is in the HTML <footer> section.
LocationFooter AssetLocation = "footer"
// LocationBody is at the bottom of the HTML <body> section.
LocationBody AssetLocation = "body"
// AssetStylesheet is a stylesheet element.
AssetStylesheet AssetType = "stylesheet"
// AssetJavaScript is a javascript element.
AssetJavaScript AssetType = "javascript"
// AssetGeneric is a generic element.
AssetGeneric AssetType = "generic"
// AuthAll is both anonymous and authenticated users.
AuthAll AuthType = "all" // Default.
// AuthAnonymousOnly is only non-authenticated users.
AuthAnonymousOnly AuthType = "anonymous"
// AuthOnly is only authenticated users.
AuthOnly AuthType = "authenticated"
// LayoutPage is a page layout.
LayoutPage LayoutType = "page"
// LayoutPost is a post layout.
LayoutPost LayoutType = "post"
)
// Asset represents an HTML asset like a stylesheet or javascript file.
type Asset struct {
// Filetype is the type of asset: generic, stylesheet, or javascript. (required)
Filetype AssetType `json:"filetype"`
// Location is the location on the HTML page where the asset will be
// added. (required)
Location AssetLocation `json:"location"`
// Auth determines whether to show the asset to all users, only authenticated
// users, or only non-authenticated users. Will display to all users if
// not specified. (optional)
Auth AuthType `json:"auth"`
// Attributes are a list of HTML attributes on all filetypes except on
// generic with no TagName. (optional)
Attributes []Attribute `json:"attributes"`
// LayoutOnly are a list of layout types where the element will be added.
// Supports page and post. Will display on all layouts if not specified.
// (optional)
LayoutOnly []LayoutType `json:"layout"`
// TagName is only for generic assets when Inline is true. Will specify the
// type of element to create. If empty, then the asset will be written to
// the page without a surrounding HTML element.
TagName string `json:"tagname"`
// ClosingTag, if true, will add a closing tag. It's only for generic assets
// when inline is false.
ClosingTag bool `json:"closingtag"`
// External, if true, will just use the path as the source of the element.
// It is only for stylesheet and javascript filetypes.
External bool `json:"external"`
// Inline if true, will output the contents from an embedded file (Path) or
// the contents (Content) after doing a find/replace (Replace).
Inline bool `json:"inline"`
// SkipExistCheck if true, will not check for the file existing because it's
// managed by a route.
SkipExistCheck bool `json:"skipexist"`
// Path is relative path to the embedded file or the full path to the
// external asset. (optional)
Path string `json:"path"`
// Content is the content that will output on the page. Path must be empty
// for content to be used and content is only used when Inline is true.
Content string `json:"content"`
// Replace is a list of find and replace strings that are run on the Path
// or Content when Inline is true.
Replace []Replace `json:"replace"`
}
// Replace represents text to find and replace.
type Replace struct {
Find string
Replace string
}
// Attribute represents an HTML attribute.
type Attribute struct {
Name string
Value interface{}
}
// Routable returns true if the file can be served from the embedded filesystem.
func (file Asset) Routable() bool {
if file.External || file.Inline || file.Filetype == AssetGeneric {
return false
}
return true
}
// SanitizedPath returns an HTML escaped asset path.
func (file Asset) SanitizedPath() string {
return html.EscapeString(file.Path)
}
// Element returns an HTML element.
func (file *Asset) Element(logger AppLogger, v Plugin, assets fs.FS, debug bool) string {
// Build the attributes.
attrs := make([]string, 0)
for _, attr := range file.Attributes {
if attr.Value == nil {
attrs = append(attrs, fmt.Sprintf(`%v`, html.EscapeString(attr.Name)))
} else {
attrs = append(attrs, fmt.Sprintf(`%v="%v"`, html.EscapeString(attr.Name), html.EscapeString(fmt.Sprint(attr.Value))))
}
}
if debug {
attrs = append(attrs, fmt.Sprintf(`%v="%v"`, html.EscapeString("data-ambplugin"), html.EscapeString(fmt.Sprint(v.PluginName()))))
}
attrsJoined := strings.Join(attrs, " ")
if len(attrsJoined) > 0 {
// Add a space at the beginning.
attrsJoined = " " + attrsJoined
}
// Get the URL prefix for assets.
urlprefix := os.Getenv("AMB_URL_PREFIX")
if len(urlprefix) == 0 {
urlprefix = ""
}
txt := ""
switch file.Filetype {
case AssetStylesheet:
if file.Inline {
ff, status, err := file.Contents(assets)
if status != http.StatusOK {
logger.Error("plugin injector: error getting file contents: %v", err.Error())
return ""
}
txt = fmt.Sprintf("<style%v>%v</style>", attrsJoined, string(ff))
} else {
if file.External {
txt = fmt.Sprintf(`<link rel="stylesheet" href="%v"%v>`, file.SanitizedPath(), attrsJoined)
} else {
txt = fmt.Sprintf(`<link rel="stylesheet" href="%v/plugins/%v/%v?v=%v"%v>`, urlprefix, v.PluginName(), file.SanitizedPath(), v.PluginVersion(), attrsJoined)
}
}
case AssetJavaScript:
if file.Inline {
ff, status, err := file.Contents(assets)
if status != http.StatusOK {
logger.Error("plugin injector: error getting file contents: %v", err.Error())
return ""
}
txt = fmt.Sprintf("<script%v>%v</script>", attrsJoined, string(ff))
} else {
if file.External {
txt = fmt.Sprintf(`<script type="application/javascript" src="%v"%v></script>`, file.SanitizedPath(), attrsJoined)
} else {
txt = fmt.Sprintf(`<script type="application/javascript" src="%v/plugins/%v/%v?v=%v"%v></script>`, urlprefix, v.PluginName(), file.SanitizedPath(), v.PluginVersion(), attrsJoined)
}
}
case AssetGeneric:
if file.Inline {
ff, status, err := file.Contents(assets)
if status != http.StatusOK {
if err != nil {
logger.Error("plugin injector: error getting file contents: %v %v", status, err.Error())
} else {
logger.Error("plugin injector: error getting file contents: %v", status)
}
return ""
}
if file.TagName == "" {
if debug {
txt = fmt.Sprintf(`<span%v data-amblocation="start"></span>%v<span%v data-amblocation="end"></span>`, attrsJoined, string(ff), attrsJoined)
} else {
txt = fmt.Sprintf(`%v`, string(ff))
}
} else {
txt = fmt.Sprintf(`<%v%v>%v</%v>`, html.EscapeString(file.TagName), attrsJoined, string(ff), html.EscapeString(file.TagName))
}
} else {
if file.ClosingTag {
txt = fmt.Sprintf(`<%v%v></%v>`, html.EscapeString(file.TagName), attrsJoined, html.EscapeString(file.TagName))
} else {
txt = fmt.Sprintf(`<%v%v>`, html.EscapeString(file.TagName), attrsJoined)
}
}
default:
logger.Error("plugin injector: unsupported asset filetype for plugin (%v): %v", v.PluginName(), file.Filetype)
}
return txt
}
// Contents returns the text of the file to inline in HTML after doing replace.
func (file *Asset) Contents(assets fs.FS) (ff []byte, status int, err error) {
// Get the contents from the path if the content field is not filled in.
if len(file.Path) > 0 {
// Use the root folder.
fsys, err := fs.Sub(assets, ".")
if err != nil {
return nil, http.StatusInternalServerError, err
}
// Open the file.
f, err := fsys.Open(file.Path)
if err != nil {
return nil, http.StatusNotFound, nil
}
defer f.Close()
// Get the contents.
fbuf := bytes.NewBuffer(nil)
_, err = io.Copy(fbuf, f)
if err != nil {
return nil, http.StatusInternalServerError, err
}
ff = fbuf.Bytes()
} else {
ff = []byte(file.Content)
}
// Loop over the items to replace.
for _, rep := range file.Replace {
ff = bytes.ReplaceAll(ff, []byte(rep.Find), []byte(rep.Replace))
}
return ff, http.StatusOK, nil
}
// AuthAssetAllowed return true if the user has access to the asset.
func AuthAssetAllowed(loggedIn bool, f Asset) bool {
switch true {
case f.Auth == AuthOnly && !loggedIn:
return false
case f.Auth == AuthOnly && loggedIn:
return true
case f.Auth == AuthAnonymousOnly && !loggedIn:
return true
case f.Auth == AuthAnonymousOnly && loggedIn:
return false
}
//f.Auth == All:
return true
}