code.gitea.io/gitea@v1.22.3/services/context/base.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package context 5 6 import ( 7 "context" 8 "fmt" 9 "html/template" 10 "io" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 "time" 16 17 "code.gitea.io/gitea/modules/httplib" 18 "code.gitea.io/gitea/modules/json" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/optional" 21 "code.gitea.io/gitea/modules/translation" 22 "code.gitea.io/gitea/modules/web/middleware" 23 24 "github.com/go-chi/chi/v5" 25 ) 26 27 type contextValuePair struct { 28 key any 29 valueFn func() any 30 } 31 32 type Base struct { 33 originCtx context.Context 34 contextValues []contextValuePair 35 36 Resp ResponseWriter 37 Req *http.Request 38 39 // Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. 40 // Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler 41 Data middleware.ContextData 42 43 // Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation 44 Locale translation.Locale 45 } 46 47 func (b *Base) Deadline() (deadline time.Time, ok bool) { 48 return b.originCtx.Deadline() 49 } 50 51 func (b *Base) Done() <-chan struct{} { 52 return b.originCtx.Done() 53 } 54 55 func (b *Base) Err() error { 56 return b.originCtx.Err() 57 } 58 59 func (b *Base) Value(key any) any { 60 for _, pair := range b.contextValues { 61 if pair.key == key { 62 return pair.valueFn() 63 } 64 } 65 return b.originCtx.Value(key) 66 } 67 68 func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any { 69 b.contextValues = append(b.contextValues, contextValuePair{key, valueFn}) 70 return b 71 } 72 73 func (b *Base) AppendContextValue(key, value any) any { 74 b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }}) 75 return b 76 } 77 78 func (b *Base) GetData() middleware.ContextData { 79 return b.Data 80 } 81 82 // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header 83 func (b *Base) AppendAccessControlExposeHeaders(names ...string) { 84 val := b.RespHeader().Get("Access-Control-Expose-Headers") 85 if len(val) != 0 { 86 b.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) 87 } else { 88 b.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) 89 } 90 } 91 92 // SetTotalCountHeader set "X-Total-Count" header 93 func (b *Base) SetTotalCountHeader(total int64) { 94 b.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) 95 b.AppendAccessControlExposeHeaders("X-Total-Count") 96 } 97 98 // Written returns true if there are something sent to web browser 99 func (b *Base) Written() bool { 100 return b.Resp.WrittenStatus() != 0 101 } 102 103 func (b *Base) WrittenStatus() int { 104 return b.Resp.WrittenStatus() 105 } 106 107 // Status writes status code 108 func (b *Base) Status(status int) { 109 b.Resp.WriteHeader(status) 110 } 111 112 // Write writes data to web browser 113 func (b *Base) Write(bs []byte) (int, error) { 114 return b.Resp.Write(bs) 115 } 116 117 // RespHeader returns the response header 118 func (b *Base) RespHeader() http.Header { 119 return b.Resp.Header() 120 } 121 122 // Error returned an error to web browser 123 func (b *Base) Error(status int, contents ...string) { 124 v := http.StatusText(status) 125 if len(contents) > 0 { 126 v = contents[0] 127 } 128 http.Error(b.Resp, v, status) 129 } 130 131 // JSON render content as JSON 132 func (b *Base) JSON(status int, content any) { 133 b.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") 134 b.Resp.WriteHeader(status) 135 if err := json.NewEncoder(b.Resp).Encode(content); err != nil { 136 log.Error("Render JSON failed: %v", err) 137 } 138 } 139 140 // RemoteAddr returns the client machine ip address 141 func (b *Base) RemoteAddr() string { 142 return b.Req.RemoteAddr 143 } 144 145 // Params returns the param on route 146 func (b *Base) Params(p string) string { 147 s, _ := url.PathUnescape(chi.URLParam(b.Req, strings.TrimPrefix(p, ":"))) 148 return s 149 } 150 151 func (b *Base) PathParamRaw(p string) string { 152 return chi.URLParam(b.Req, strings.TrimPrefix(p, ":")) 153 } 154 155 // ParamsInt64 returns the param on route as int64 156 func (b *Base) ParamsInt64(p string) int64 { 157 v, _ := strconv.ParseInt(b.Params(p), 10, 64) 158 return v 159 } 160 161 // SetParams set params into routes 162 func (b *Base) SetParams(k, v string) { 163 chiCtx := chi.RouteContext(b) 164 chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) 165 } 166 167 // FormString returns the first value matching the provided key in the form as a string 168 func (b *Base) FormString(key string) string { 169 return b.Req.FormValue(key) 170 } 171 172 // FormStrings returns a string slice for the provided key from the form 173 func (b *Base) FormStrings(key string) []string { 174 if b.Req.Form == nil { 175 if err := b.Req.ParseMultipartForm(32 << 20); err != nil { 176 return nil 177 } 178 } 179 if v, ok := b.Req.Form[key]; ok { 180 return v 181 } 182 return nil 183 } 184 185 // FormTrim returns the first value for the provided key in the form as a space trimmed string 186 func (b *Base) FormTrim(key string) string { 187 return strings.TrimSpace(b.Req.FormValue(key)) 188 } 189 190 // FormInt returns the first value for the provided key in the form as an int 191 func (b *Base) FormInt(key string) int { 192 v, _ := strconv.Atoi(b.Req.FormValue(key)) 193 return v 194 } 195 196 // FormInt64 returns the first value for the provided key in the form as an int64 197 func (b *Base) FormInt64(key string) int64 { 198 v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) 199 return v 200 } 201 202 // FormBool returns true if the value for the provided key in the form is "1", "true" or "on" 203 func (b *Base) FormBool(key string) bool { 204 s := b.Req.FormValue(key) 205 v, _ := strconv.ParseBool(s) 206 v = v || strings.EqualFold(s, "on") 207 return v 208 } 209 210 // FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value 211 // for the provided key exists in the form else it returns optional.None[bool]() 212 func (b *Base) FormOptionalBool(key string) optional.Option[bool] { 213 value := b.Req.FormValue(key) 214 if len(value) == 0 { 215 return optional.None[bool]() 216 } 217 s := b.Req.FormValue(key) 218 v, _ := strconv.ParseBool(s) 219 v = v || strings.EqualFold(s, "on") 220 return optional.Some(v) 221 } 222 223 func (b *Base) SetFormString(key, value string) { 224 _ = b.Req.FormValue(key) // force parse form 225 b.Req.Form.Set(key, value) 226 } 227 228 // PlainTextBytes renders bytes as plain text 229 func (b *Base) plainTextInternal(skip, status int, bs []byte) { 230 statusPrefix := status / 100 231 if statusPrefix == 4 || statusPrefix == 5 { 232 log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) 233 } 234 b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") 235 b.Resp.Header().Set("X-Content-Type-Options", "nosniff") 236 b.Resp.WriteHeader(status) 237 _, _ = b.Resp.Write(bs) 238 } 239 240 // PlainTextBytes renders bytes as plain text 241 func (b *Base) PlainTextBytes(status int, bs []byte) { 242 b.plainTextInternal(2, status, bs) 243 } 244 245 // PlainText renders content as plain text 246 func (b *Base) PlainText(status int, text string) { 247 b.plainTextInternal(2, status, []byte(text)) 248 } 249 250 // Redirect redirects the request 251 func (b *Base) Redirect(location string, status ...int) { 252 code := http.StatusSeeOther 253 if len(status) == 1 { 254 code = status[0] 255 } 256 257 if !httplib.IsRelativeURL(location) { 258 // Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path 259 // 1. the first request to "/my-path" contains cookie 260 // 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) 261 // 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser 262 // 4. then the browser accepts the empty session, then the user is logged out 263 // So in this case, we should remove the session cookie from the response header 264 removeSessionCookieHeader(b.Resp) 265 } 266 // in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx 267 if b.Req.Header.Get("HX-Request") == "true" { 268 b.Resp.Header().Set("HX-Redirect", location) 269 // we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect 270 // so as to give htmx redirect logic a chance to run 271 b.Status(http.StatusNoContent) 272 return 273 } 274 http.Redirect(b.Resp, b.Req, location, code) 275 } 276 277 type ServeHeaderOptions httplib.ServeHeaderOptions 278 279 func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { 280 httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt)) 281 } 282 283 // ServeContent serves content to http request 284 func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { 285 httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts)) 286 http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) 287 } 288 289 // Close frees all resources hold by Context 290 func (b *Base) cleanUp() { 291 if b.Req != nil && b.Req.MultipartForm != nil { 292 _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory 293 } 294 } 295 296 func (b *Base) Tr(msg string, args ...any) template.HTML { 297 return b.Locale.Tr(msg, args...) 298 } 299 300 func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { 301 return b.Locale.TrN(cnt, key1, keyN, args...) 302 } 303 304 func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { 305 b = &Base{ 306 originCtx: req.Context(), 307 Req: req, 308 Resp: WrapResponseWriter(resp), 309 Locale: middleware.Locale(resp, req), 310 Data: middleware.GetContextData(req.Context()), 311 } 312 b.Req = b.Req.WithContext(b) 313 b.AppendContextValue(translation.ContextKey, b.Locale) 314 b.AppendContextValue(httplib.RequestContextKey, b.Req) 315 return b, b.cleanUp 316 }