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  }