2023-05-21 09:50:53 +08:00
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"context"
"fmt"
2024-02-15 05:48:45 +08:00
"html/template"
2023-05-21 09:50:53 +08:00
"io"
"net/http"
"strings"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
2024-12-24 11:43:57 +08:00
"code.gitea.io/gitea/modules/reqctx"
2024-06-18 07:28:47 +08:00
"code.gitea.io/gitea/modules/setting"
2023-05-21 09:50:53 +08:00
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware"
)
2024-11-13 16:58:09 +08:00
type BaseContextKeyType struct { }
var BaseContextKey BaseContextKeyType
2023-05-21 09:50:53 +08:00
type Base struct {
2024-12-24 11:43:57 +08:00
context . Context
reqctx . RequestDataStore
2023-05-21 09:50:53 +08:00
Resp ResponseWriter
Req * http . Request
// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData.
// Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler
2024-12-24 11:43:57 +08:00
Data reqctx . ContextData
2023-05-21 09:50:53 +08:00
// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
Locale translation . Locale
}
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
func ( b * Base ) AppendAccessControlExposeHeaders ( names ... string ) {
val := b . RespHeader ( ) . Get ( "Access-Control-Expose-Headers" )
if len ( val ) != 0 {
b . RespHeader ( ) . Set ( "Access-Control-Expose-Headers" , fmt . Sprintf ( "%s, %s" , val , strings . Join ( names , ", " ) ) )
} else {
b . RespHeader ( ) . Set ( "Access-Control-Expose-Headers" , strings . Join ( names , ", " ) )
}
}
// SetTotalCountHeader set "X-Total-Count" header
func ( b * Base ) SetTotalCountHeader ( total int64 ) {
b . RespHeader ( ) . Set ( "X-Total-Count" , fmt . Sprint ( total ) )
b . AppendAccessControlExposeHeaders ( "X-Total-Count" )
}
// Written returns true if there are something sent to web browser
func ( b * Base ) Written ( ) bool {
2023-06-18 15:59:09 +08:00
return b . Resp . WrittenStatus ( ) != 0
}
func ( b * Base ) WrittenStatus ( ) int {
return b . Resp . WrittenStatus ( )
2023-05-21 09:50:53 +08:00
}
// Status writes status code
func ( b * Base ) Status ( status int ) {
b . Resp . WriteHeader ( status )
}
// Write writes data to web browser
func ( b * Base ) Write ( bs [ ] byte ) ( int , error ) {
return b . Resp . Write ( bs )
}
// RespHeader returns the response header
func ( b * Base ) RespHeader ( ) http . Header {
return b . Resp . Header ( )
}
// Error returned an error to web browser
func ( b * Base ) Error ( status int , contents ... string ) {
v := http . StatusText ( status )
if len ( contents ) > 0 {
v = contents [ 0 ]
}
http . Error ( b . Resp , v , status )
}
// JSON render content as JSON
2023-07-04 20:36:08 +02:00
func ( b * Base ) JSON ( status int , content any ) {
2023-05-21 09:50:53 +08:00
b . Resp . Header ( ) . Set ( "Content-Type" , "application/json;charset=utf-8" )
b . Resp . WriteHeader ( status )
if err := json . NewEncoder ( b . Resp ) . Encode ( content ) ; err != nil {
log . Error ( "Render JSON failed: %v" , err )
}
}
// RemoteAddr returns the client machine ip address
func ( b * Base ) RemoteAddr ( ) string {
return b . Req . RemoteAddr
}
// PlainTextBytes renders bytes as plain text
func ( b * Base ) plainTextInternal ( skip , status int , bs [ ] byte ) {
statusPrefix := status / 100
if statusPrefix == 4 || statusPrefix == 5 {
log . Log ( skip , log . TRACE , "plainTextInternal (status=%d): %s" , status , string ( bs ) )
}
b . Resp . Header ( ) . Set ( "Content-Type" , "text/plain;charset=utf-8" )
b . Resp . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
b . Resp . WriteHeader ( status )
2024-05-03 10:39:36 +08:00
_ , _ = b . Resp . Write ( bs )
2023-05-21 09:50:53 +08:00
}
// PlainTextBytes renders bytes as plain text
func ( b * Base ) PlainTextBytes ( status int , bs [ ] byte ) {
b . plainTextInternal ( 2 , status , bs )
}
// PlainText renders content as plain text
func ( b * Base ) PlainText ( status int , text string ) {
b . plainTextInternal ( 2 , status , [ ] byte ( text ) )
}
// Redirect redirects the request
func ( b * Base ) Redirect ( location string , status ... int ) {
code := http . StatusSeeOther
if len ( status ) == 1 {
code = status [ 0 ]
}
2024-05-07 16:26:13 +08:00
if ! httplib . IsRelativeURL ( location ) {
2023-05-21 09:50:53 +08:00
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
// 1. the first request to "/my-path" contains cookie
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
// 4. then the browser accepts the empty session, then the user is logged out
// So in this case, we should remove the session cookie from the response header
removeSessionCookieHeader ( b . Resp )
}
2024-02-26 14:40:41 +02:00
// in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
if b . Req . Header . Get ( "HX-Request" ) == "true" {
b . Resp . Header ( ) . Set ( "HX-Redirect" , location )
// we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
// so as to give htmx redirect logic a chance to run
b . Status ( http . StatusNoContent )
return
}
2023-05-21 09:50:53 +08:00
http . Redirect ( b . Resp , b . Req , location , code )
}
type ServeHeaderOptions httplib . ServeHeaderOptions
func ( b * Base ) SetServeHeaders ( opt * ServeHeaderOptions ) {
httplib . ServeSetHeaders ( b . Resp , ( * httplib . ServeHeaderOptions ) ( opt ) )
}
// ServeContent serves content to http request
func ( b * Base ) ServeContent ( r io . ReadSeeker , opts * ServeHeaderOptions ) {
httplib . ServeSetHeaders ( b . Resp , ( * httplib . ServeHeaderOptions ) ( opts ) )
http . ServeContent ( b . Resp , b . Req , opts . Filename , opts . LastModified , r )
}
2024-02-15 05:48:45 +08:00
func ( b * Base ) Tr ( msg string , args ... any ) template . HTML {
2023-05-21 09:50:53 +08:00
return b . Locale . Tr ( msg , args ... )
}
2024-02-15 05:48:45 +08:00
func ( b * Base ) TrN ( cnt any , key1 , keyN string , args ... any ) template . HTML {
2023-05-21 09:50:53 +08:00
return b . Locale . TrN ( cnt , key1 , keyN , args ... )
}
2024-12-24 11:43:57 +08:00
func NewBaseContext ( resp http . ResponseWriter , req * http . Request ) * Base {
ds := reqctx . GetRequestDataStore ( req . Context ( ) )
b := & Base {
Context : req . Context ( ) ,
RequestDataStore : ds ,
Req : req ,
Resp : WrapResponseWriter ( resp ) ,
Locale : middleware . Locale ( resp , req ) ,
Data : ds . GetData ( ) ,
2023-05-21 09:50:53 +08:00
}
b . Req = b . Req . WithContext ( b )
2024-12-24 11:43:57 +08:00
ds . SetContextValue ( BaseContextKey , b )
ds . SetContextValue ( translation . ContextKey , b . Locale )
ds . SetContextValue ( httplib . RequestContextKey , b . Req )
return b
}
func NewBaseContextForTest ( resp http . ResponseWriter , req * http . Request ) * Base {
if ! setting . IsInTesting {
panic ( "This function is only for testing" )
}
ctx := reqctx . NewRequestContextForTest ( req . Context ( ) )
* req = * req . WithContext ( ctx )
return NewBaseContext ( resp , req )
2023-05-21 09:50:53 +08:00
}