github.com/apex/up@v1.7.1/internal/inject/inject.go (about) 1 // Package inject provides script and style injection utilities. 2 package inject 3 4 import ( 5 "encoding/json" 6 "html" 7 "io/ioutil" 8 "strings" 9 10 "github.com/apex/log" 11 "github.com/apex/up/internal/validate" 12 "github.com/pkg/errors" 13 ) 14 15 // TODO: template support 16 // TODO: move config to "config" pkg 17 18 // locations valid. 19 var locations = []string{ 20 "head", 21 "body", 22 } 23 24 // types valid. 25 var types = []string{ 26 "literal", 27 "comment", 28 "style", 29 "script", 30 "inline style", 31 "inline script", 32 "google analytics", 33 "segment", 34 } 35 36 // Rules is a set of rules mapped by location. 37 type Rules map[string][]*Rule 38 39 // Default rules. 40 func (r Rules) Default() error { 41 for pos, rules := range r { 42 for i, rule := range rules { 43 if err := rule.Default(); err != nil { 44 return errors.Wrapf(err, "%s rule #%d", pos, i+1) 45 } 46 } 47 } 48 49 return nil 50 } 51 52 // Validate rules. 53 func (r Rules) Validate() error { 54 for pos, rules := range r { 55 if err := validate.List(pos, locations); err != nil { 56 return errors.Wrap(err, "invalid location") 57 } 58 59 for i, rule := range rules { 60 if err := rule.Validate(); err != nil { 61 return errors.Wrapf(err, "%s rule #%d", pos, i+1) 62 } 63 } 64 } 65 66 return nil 67 } 68 69 // Apply rules to html. 70 func (r Rules) Apply(html string) string { 71 for pos, rules := range r { 72 log.Debugf("injecting %s rules", pos) 73 for _, rule := range rules { 74 log.Debugf(" inject %s %q", rule.Type, rule.Value) 75 switch pos { 76 case "head": 77 html = Head(html, rule.Apply(html)) 78 case "body": 79 html = Body(html, rule.Apply(html)) 80 } 81 } 82 } 83 84 return html 85 } 86 87 // Rule is an injection rule. 88 type Rule struct { 89 // Type of injection, defaults to "literal" unless File is used, 90 // or Value contains .js or .css extensions. 91 Type string `json:"type"` 92 93 // Value is the literal, inline string, or src/href of the injected tag. 94 Value string `json:"value"` 95 96 // File is used to load source from disk instead of providing Value. Note 97 // that if Type is not explicitly provided, then it will default to 98 // "inline script" or "inline style" for .js and .css files respectively. 99 File string `json:"file"` 100 } 101 102 // Apply rule to html. 103 func (r *Rule) Apply(html string) string { 104 switch r.Type { 105 case "literal": 106 return r.Value 107 case "script": 108 return Script(r.Value) 109 case "style": 110 return Style(r.Value) 111 case "inline script": 112 return ScriptInline(r.Value) 113 case "inline style": 114 return StyleInline(r.Value) 115 case "comment": 116 return Comment(r.Value) 117 case "segment": 118 return Segment(r.Value) 119 case "google analytics": 120 return GoogleAnalytics(r.Value) 121 default: 122 return "" 123 } 124 } 125 126 // Default applies defaults. 127 func (r *Rule) Default() error { 128 if r.Type == "" { 129 r.Type = "literal" 130 } 131 132 if r.File != "" { 133 if err := r.defaultFile(r.File); err != nil { 134 return err 135 } 136 } 137 138 return nil 139 } 140 141 // Validate returns an error if incorrect. 142 func (r *Rule) Validate() error { 143 if err := validate.List(r.Type, types); err != nil { 144 return errors.Wrap(err, "invalid .type") 145 } 146 147 if strings.TrimSpace(r.Value) == "" { 148 return errors.Errorf(`.value is required`) 149 } 150 151 return nil 152 } 153 154 // defaultFile defaults value" from the given path. 155 func (r *Rule) defaultFile(path string) error { 156 b, err := ioutil.ReadFile(path) 157 if err != nil { 158 return err 159 } 160 161 r.Value = string(b) 162 return nil 163 } 164 165 // Head injects a string before the closing head tag. 166 func Head(html, s string) string { 167 return strings.Replace(html, "</head>", " "+s+"\n </head>", 1) 168 } 169 170 // Body injects a string before the closing body tag. 171 func Body(html, s string) string { 172 return strings.Replace(html, "</body>", " "+s+"\n </body>", 1) 173 } 174 175 // Script returns an script. 176 func Script(src string) string { 177 return `<script src="` + html.EscapeString(src) + `"></script>` 178 } 179 180 // ScriptInline returns an inline script. 181 func ScriptInline(s string) string { 182 return `<script>` + s + `</script>` 183 } 184 185 // Style returns an style. 186 func Style(href string) string { 187 return `<link rel="stylesheet" href="` + html.EscapeString(href) + `">` 188 } 189 190 // StyleInline returns an inline style. 191 func StyleInline(s string) string { 192 return `<style>` + s + `</style>` 193 } 194 195 // Comment returns an html comment. 196 func Comment(s string) string { 197 return "<!-- " + html.EscapeString(s) + " -->" 198 } 199 200 // Segment inline script with key. 201 func Segment(key string) string { 202 return ScriptInline(` 203 !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="4.0.0"; 204 analytics.load("` + key + `"); 205 analytics.page(); 206 }}(); 207 `) 208 } 209 210 // GoogleAnalytics inline script with tracking key. 211 func GoogleAnalytics(trackingID string) string { 212 return ScriptInline(` 213 (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 214 (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 215 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 216 })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 217 218 ga('create', '` + trackingID + `', 'auto'); 219 ga('send', 'pageview'); 220 `) 221 } 222 223 // Var injection. 224 func Var(kind, name string, v interface{}) string { 225 b, _ := json.Marshal(v) 226 return ScriptInline(kind + ` ` + name + ` = ` + string(b)) 227 }