2014-03-15 19:01:50 +08:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2020-01-10 05:34:25 +08:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-28 02:20:29 +08:00
// SPDX-License-Identifier: MIT
2014-03-15 19:01:50 +08:00
2016-03-12 00:56:52 +08:00
package context
2014-03-15 19:01:50 +08:00
import (
2021-01-26 23:36:53 +08:00
"context"
"encoding/hex"
2022-02-07 03:28:25 +08:00
"errors"
2022-04-08 02:59:56 +08:00
"fmt"
2016-11-30 05:49:06 +08:00
"html"
2014-03-23 01:44:02 +08:00
"html/template"
2014-04-16 00:27:29 +08:00
"io"
2022-02-07 03:28:25 +08:00
"net"
2014-03-15 19:01:50 +08:00
"net/http"
2018-03-16 05:13:34 +08:00
"net/url"
2017-06-26 09:06:40 +08:00
"path"
2023-03-21 04:56:48 +08:00
"regexp"
2021-01-26 23:36:53 +08:00
"strconv"
2014-03-23 04:40:09 +08:00
"strings"
2023-03-21 04:56:48 +08:00
texttemplate "text/template"
2014-03-19 21:57:55 +08:00
"time"
2014-03-15 19:01:50 +08:00
2022-04-08 02:59:56 +08:00
"code.gitea.io/gitea/models/db"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2016-11-11 00:24:48 +08:00
"code.gitea.io/gitea/modules/base"
2021-01-27 22:56:54 +08:00
mc "code.gitea.io/gitea/modules/cache"
2022-01-20 07:26:57 +08:00
"code.gitea.io/gitea/modules/git"
2022-07-23 14:38:03 +08:00
"code.gitea.io/gitea/modules/httpcache"
2021-07-25 00:03:58 +08:00
"code.gitea.io/gitea/modules/json"
2016-11-11 00:24:48 +08:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2021-01-26 23:36:53 +08:00
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
2022-11-18 01:55:15 +08:00
"code.gitea.io/gitea/modules/typesniffer"
2022-04-01 16:47:50 +08:00
"code.gitea.io/gitea/modules/util"
2021-01-30 16:55:53 +08:00
"code.gitea.io/gitea/modules/web/middleware"
2019-08-24 00:40:30 +08:00
2021-01-26 23:36:53 +08:00
"gitea.com/go-chi/cache"
"gitea.com/go-chi/session"
2021-10-14 10:50:23 +08:00
chi "github.com/go-chi/chi/v5"
2023-02-23 03:21:46 +08:00
"github.com/minio/sha256-simd"
2021-01-26 23:36:53 +08:00
"golang.org/x/crypto/pbkdf2"
2014-03-15 19:01:50 +08:00
)
2021-01-26 23:36:53 +08:00
// Render represents a template render
type Render interface {
2023-04-08 21:15:22 +08:00
TemplateLookup ( tmpl string ) ( * template . Template , error )
2023-04-08 14:21:50 +08:00
HTML ( w io . Writer , status int , name string , data interface { } ) error
2021-01-26 23:36:53 +08:00
}
2014-03-15 21:17:16 +08:00
// Context represents context of a request.
2014-03-15 19:01:50 +08:00
type Context struct {
2021-10-13 02:11:35 +08:00
Resp ResponseWriter
Req * http . Request
Data map [ string ] interface { } // data used by MVC templates
2021-10-15 10:35:26 +08:00
PageData map [ string ] interface { } // data used by JavaScript modules in one page, it's `window.config.pageData`
2021-10-13 02:11:35 +08:00
Render Render
2021-01-26 23:36:53 +08:00
translation . Locale
2014-08-01 05:25:34 +08:00
Cache cache . Cache
2022-04-08 13:21:05 +08:00
csrf CSRFProtector
2021-01-30 16:55:53 +08:00
Flash * middleware . Flash
2014-07-26 12:24:27 +08:00
Session session . Store
2017-06-26 09:06:40 +08:00
Link string // current request URL
2017-11-28 17:43:51 +08:00
EscapedLink string
2022-03-22 15:03:22 +08:00
Doer * user_model . User
2014-11-19 00:07:16 +08:00
IsSigned bool
IsBasicAuth bool
2014-03-16 00:03:23 +08:00
2022-03-26 17:04:22 +08:00
ContextUser * user_model . User
Repo * Repository
Org * Organization
2022-03-30 16:42:47 +08:00
Package * Package
2014-03-15 19:01:50 +08:00
}
2022-05-05 22:13:23 +08:00
// Close frees all resources hold by Context
func ( ctx * Context ) Close ( ) error {
var err error
if ctx . Req != nil && ctx . Req . MultipartForm != nil {
err = ctx . Req . MultipartForm . RemoveAll ( ) // remove the temp files buffered to tmp directory
}
// TODO: close opened repo, and more
return err
}
2021-11-17 02:18:25 +08:00
// TrHTMLEscapeArgs runs Tr but pre-escapes all arguments with html.EscapeString.
// This is useful if the locale message is intended to only produce HTML content.
func ( ctx * Context ) TrHTMLEscapeArgs ( msg string , args ... string ) string {
trArgs := make ( [ ] interface { } , len ( args ) )
for i , arg := range args {
trArgs [ i ] = html . EscapeString ( arg )
}
return ctx . Tr ( msg , trArgs ... )
}
2021-01-05 21:05:40 +08:00
// GetData returns the data
func ( ctx * Context ) GetData ( ) map [ string ] interface { } {
return ctx . Data
}
2019-04-08 06:49:34 +08:00
// IsUserSiteAdmin returns true if current user is a site admin
func ( ctx * Context ) IsUserSiteAdmin ( ) bool {
2022-03-22 15:03:22 +08:00
return ctx . IsSigned && ctx . Doer . IsAdmin
2019-04-08 06:49:34 +08:00
}
// IsUserRepoOwner returns true if current user owns current repo
func ( ctx * Context ) IsUserRepoOwner ( ) bool {
return ctx . Repo . IsOwner ( )
}
// IsUserRepoAdmin returns true if current user is admin in current repo
func ( ctx * Context ) IsUserRepoAdmin ( ) bool {
return ctx . Repo . IsAdmin ( )
}
// IsUserRepoWriter returns true if current user has write privilege in current repo
2021-11-10 03:57:58 +08:00
func ( ctx * Context ) IsUserRepoWriter ( unitTypes [ ] unit . Type ) bool {
2019-04-08 06:49:34 +08:00
for _ , unitType := range unitTypes {
if ctx . Repo . CanWrite ( unitType ) {
return true
}
}
return false
}
// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part
2021-11-10 03:57:58 +08:00
func ( ctx * Context ) IsUserRepoReaderSpecific ( unitType unit . Type ) bool {
2019-04-08 06:49:34 +08:00
return ctx . Repo . CanRead ( unitType )
}
// IsUserRepoReaderAny returns true if current user can read any part of current repo
func ( ctx * Context ) IsUserRepoReaderAny ( ) bool {
return ctx . Repo . HasAccess ( )
}
2021-01-24 23:23:05 +08:00
// RedirectToUser redirect to a differently-named user
func RedirectToUser ( ctx * Context , userName string , redirectUserID int64 ) {
2022-12-03 10:48:26 +08:00
user , err := user_model . GetUserByID ( ctx , redirectUserID )
2021-01-24 23:23:05 +08:00
if err != nil {
ctx . ServerError ( "GetUserByID" , err )
return
}
redirectPath := strings . Replace (
2021-11-17 02:18:25 +08:00
ctx . Req . URL . EscapedPath ( ) ,
url . PathEscape ( userName ) ,
url . PathEscape ( user . Name ) ,
2021-01-24 23:23:05 +08:00
1 ,
)
if ctx . Req . URL . RawQuery != "" {
redirectPath += "?" + ctx . Req . URL . RawQuery
}
2022-03-23 12:54:07 +08:00
ctx . Redirect ( path . Join ( setting . AppSubURL , redirectPath ) , http . StatusTemporaryRedirect )
2021-01-24 23:23:05 +08:00
}
2016-11-25 14:51:01 +08:00
// HasAPIError returns true if error occurs in form validation.
func ( ctx * Context ) HasAPIError ( ) bool {
2014-05-06 01:08:01 +08:00
hasErr , ok := ctx . Data [ "HasError" ]
if ! ok {
return false
}
return hasErr . ( bool )
}
2016-11-25 14:51:01 +08:00
// GetErrMsg returns error message
2014-05-06 01:08:01 +08:00
func ( ctx * Context ) GetErrMsg ( ) string {
return ctx . Data [ "ErrorMsg" ] . ( string )
}
2014-03-15 22:52:14 +08:00
// HasError returns true if error occurs in form validation.
2021-12-15 14:59:57 +08:00
// Attention: this function changes ctx.Data and ctx.Flash
2014-03-15 22:52:14 +08:00
func ( ctx * Context ) HasError ( ) bool {
hasErr , ok := ctx . Data [ "HasError" ]
if ! ok {
return false
}
2014-04-14 06:12:07 +08:00
ctx . Flash . ErrorMsg = ctx . Data [ "ErrorMsg" ] . ( string )
ctx . Data [ "Flash" ] = ctx . Flash
2014-03-15 22:52:14 +08:00
return hasErr . ( bool )
}
2015-07-08 19:47:56 +08:00
// HasValue returns true if value of given name exists.
func ( ctx * Context ) HasValue ( name string ) bool {
_ , ok := ctx . Data [ name ]
return ok
}
2018-03-16 05:13:34 +08:00
// RedirectToFirst redirects to first not empty URL
func ( ctx * Context ) RedirectToFirst ( location ... string ) {
for _ , loc := range location {
if len ( loc ) == 0 {
continue
}
2022-03-24 00:12:36 +08:00
// Unfortunately browsers consider a redirect Location with preceding "//" and "/\" as meaning redirect to "http(s)://REST_OF_PATH"
// Therefore we should ignore these redirect locations to prevent open redirects
if len ( loc ) > 1 && loc [ 0 ] == '/' && ( loc [ 1 ] == '/' || loc [ 1 ] == '\\' ) {
continue
}
2018-03-16 05:13:34 +08:00
u , err := url . Parse ( loc )
2020-01-10 05:34:25 +08:00
if err != nil || ( ( u . Scheme != "" || u . Host != "" ) && ! strings . HasPrefix ( strings . ToLower ( loc ) , strings . ToLower ( setting . AppURL ) ) ) {
2018-03-16 05:13:34 +08:00
continue
}
ctx . Redirect ( loc )
return
}
ctx . Redirect ( setting . AppSubURL + "/" )
}
2023-03-21 04:56:48 +08:00
var templateExecutingErr = regexp . MustCompile ( ` ^template: (.*):([1-9][0-9]*):([1-9][0-9]*): executing (?:"(.*)" at <(.*)>: )? ` )
2021-12-15 14:59:57 +08:00
// HTML calls Context.HTML and renders the template to HTTP response
2014-07-26 12:24:27 +08:00
func ( ctx * Context ) HTML ( status int , name base . TplName ) {
2015-12-20 14:06:54 +08:00
log . Debug ( "Template: %s" , name )
2021-12-15 14:59:57 +08:00
tmplStartTime := time . Now ( )
2022-02-12 01:08:22 +08:00
if ! setting . IsProd {
ctx . Data [ "TemplateName" ] = name
}
ctx . Data [ "TemplateLoadTimes" ] = func ( ) string {
2021-12-15 14:59:57 +08:00
return strconv . FormatInt ( time . Since ( tmplStartTime ) . Nanoseconds ( ) / 1e6 , 10 ) + "ms"
2021-01-29 12:33:47 +08:00
}
2022-08-08 21:42:36 +08:00
if err := ctx . Render . HTML ( ctx . Resp , status , string ( name ) , templates . BaseVars ( ) . Merge ( ctx . Data ) ) ; err != nil {
2021-02-20 12:26:57 +08:00
if status == http . StatusInternalServerError && name == base . TplName ( "status/500" ) {
2023-04-08 21:15:22 +08:00
ctx . PlainText ( http . StatusInternalServerError , "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files." )
2021-02-20 12:26:57 +08:00
return
}
2023-03-21 04:56:48 +08:00
if execErr , ok := err . ( texttemplate . ExecError ) ; ok {
if groups := templateExecutingErr . FindStringSubmatch ( err . Error ( ) ) ; len ( groups ) > 0 {
errorTemplateName , lineStr , posStr := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
target := ""
if len ( groups ) == 6 {
target = groups [ 5 ]
}
line , _ := strconv . Atoi ( lineStr ) // Cannot error out as groups[2] is [1-9][0-9]*
pos , _ := strconv . Atoi ( posStr ) // Cannot error out as groups[3] is [1-9][0-9]*
2023-04-12 18:16:45 +08:00
assetLayerName := templates . AssetFS ( ) . GetFileLayerName ( errorTemplateName + ".tmpl" )
filename := fmt . Sprintf ( "(%s) %s" , assetLayerName , errorTemplateName )
2023-03-21 04:56:48 +08:00
if errorTemplateName != string ( name ) {
filename += " (subtemplate of " + string ( name ) + ")"
}
2023-04-08 21:15:22 +08:00
err = fmt . Errorf ( "failed to render %s, error: %w:\n%s" , filename , err , templates . GetLineFromTemplate ( errorTemplateName , line , target , pos ) )
2023-03-21 04:56:48 +08:00
} else {
2023-04-12 18:16:45 +08:00
assetLayerName := templates . AssetFS ( ) . GetFileLayerName ( execErr . Name + ".tmpl" )
filename := fmt . Sprintf ( "(%s) %s" , assetLayerName , execErr . Name )
2023-03-21 04:56:48 +08:00
if execErr . Name != string ( name ) {
filename += " (subtemplate of " + string ( name ) + ")"
}
2023-04-08 21:15:22 +08:00
err = fmt . Errorf ( "failed to render %s, error: %w" , filename , err )
2023-03-21 04:56:48 +08:00
}
}
2021-01-26 23:36:53 +08:00
ctx . ServerError ( "Render failed" , err )
}
}
2021-12-15 14:59:57 +08:00
// RenderToString renders the template content to a string
func ( ctx * Context ) RenderToString ( name base . TplName , data map [ string ] interface { } ) ( string , error ) {
2021-01-26 23:36:53 +08:00
var buf strings . Builder
2022-03-23 12:54:07 +08:00
err := ctx . Render . HTML ( & buf , http . StatusOK , string ( name ) , data )
2021-01-26 23:36:53 +08:00
return buf . String ( ) , err
2014-03-20 19:50:26 +08:00
}
2014-03-15 22:52:14 +08:00
// RenderWithErr used for page has form validation but need to prompt error to users.
2014-07-26 12:24:27 +08:00
func ( ctx * Context ) RenderWithErr ( msg string , tpl base . TplName , form interface { } ) {
2014-04-04 03:50:55 +08:00
if form != nil {
2021-01-30 16:55:53 +08:00
middleware . AssignForm ( form , ctx . Data )
2014-04-04 03:50:55 +08:00
}
2014-04-11 04:36:50 +08:00
ctx . Flash . ErrorMsg = msg
ctx . Data [ "Flash" ] = ctx . Flash
2021-04-05 23:30:52 +08:00
ctx . HTML ( http . StatusOK , tpl )
2014-03-15 22:52:14 +08:00
}
2018-01-11 05:34:17 +08:00
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
2021-12-15 14:59:57 +08:00
func ( ctx * Context ) NotFound ( logMsg string , logErr error ) {
ctx . notFoundInternal ( logMsg , logErr )
2019-04-10 02:10:42 +08:00
}
2021-12-15 14:59:57 +08:00
func ( ctx * Context ) notFoundInternal ( logMsg string , logErr error ) {
if logErr != nil {
2022-01-30 04:52:37 +08:00
log . Log ( 2 , log . DEBUG , "%s: %v" , logMsg , logErr )
2021-10-20 22:37:19 +08:00
if ! setting . IsProd {
2021-12-15 14:59:57 +08:00
ctx . Data [ "ErrorMsg" ] = logErr
2014-05-02 06:53:41 +08:00
}
2014-03-19 16:48:45 +08:00
}
2021-12-15 14:59:57 +08:00
// response simple message if Accept isn't text/html
showHTML := false
for _ , part := range ctx . Req . Header [ "Accept" ] {
if strings . Contains ( part , "text/html" ) {
showHTML = true
break
2021-04-01 23:11:42 +08:00
}
2021-12-15 14:59:57 +08:00
}
2021-04-01 23:11:42 +08:00
2021-12-15 14:59:57 +08:00
if ! showHTML {
2022-01-30 04:52:37 +08:00
ctx . plainTextInternal ( 3 , http . StatusNotFound , [ ] byte ( "Not found.\n" ) )
2021-12-15 14:59:57 +08:00
return
2021-04-01 23:11:42 +08:00
}
2019-02-20 07:09:47 +08:00
ctx . Data [ "IsRepo" ] = ctx . Repo . Repository != nil
2018-01-11 05:34:17 +08:00
ctx . Data [ "Title" ] = "Page Not Found"
ctx . HTML ( http . StatusNotFound , base . TplName ( "status/404" ) )
}
2021-12-15 14:59:57 +08:00
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
func ( ctx * Context ) ServerError ( logMsg string , logErr error ) {
ctx . serverErrorInternal ( logMsg , logErr )
2019-04-10 02:10:42 +08:00
}
2021-12-15 14:59:57 +08:00
func ( ctx * Context ) serverErrorInternal ( logMsg string , logErr error ) {
if logErr != nil {
log . ErrorWithSkip ( 2 , "%s: %v" , logMsg , logErr )
2022-03-11 04:23:15 +08:00
if _ , ok := logErr . ( * net . OpError ) ; ok || errors . Is ( logErr , & net . OpError { } ) {
2022-02-07 03:28:25 +08:00
// This is an error within the underlying connection
// and further rendering will not work so just return
return
}
2021-10-20 22:37:19 +08:00
if ! setting . IsProd {
2021-12-15 14:59:57 +08:00
ctx . Data [ "ErrorMsg" ] = logErr
2018-01-11 05:34:17 +08:00
}
2014-05-02 06:53:41 +08:00
}
2018-01-11 05:34:17 +08:00
ctx . Data [ "Title" ] = "Internal Server Error"
2019-01-31 06:00:00 +08:00
ctx . HTML ( http . StatusInternalServerError , base . TplName ( "status/500" ) )
2014-03-15 19:01:50 +08:00
}
2016-08-30 17:08:38 +08:00
// NotFoundOrServerError use error check function to determine if the error
2021-12-15 14:59:57 +08:00
// is about not found. It responds with 404 status code for not found error,
2016-08-30 17:08:38 +08:00
// or error context description for logging purpose of 500 server error.
2021-12-15 14:59:57 +08:00
func ( ctx * Context ) NotFoundOrServerError ( logMsg string , errCheck func ( error ) bool , err error ) {
if errCheck ( err ) {
ctx . notFoundInternal ( logMsg , err )
2016-07-26 02:48:17 +08:00
return
}
2021-12-15 14:59:57 +08:00
ctx . serverErrorInternal ( logMsg , err )
}
2016-07-26 02:48:17 +08:00
2021-12-15 14:59:57 +08:00
// PlainTextBytes renders bytes as plain text
2022-01-30 04:52:37 +08:00
func ( ctx * Context ) 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 ) )
2021-12-15 14:59:57 +08:00
}
ctx . Resp . Header ( ) . Set ( "Content-Type" , "text/plain;charset=utf-8" )
2022-01-23 02:32:35 +08:00
ctx . Resp . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
2022-11-18 01:55:15 +08:00
ctx . Resp . WriteHeader ( status )
2021-12-15 14:59:57 +08:00
if _ , err := ctx . Resp . Write ( bs ) ; err != nil {
2022-01-30 04:52:37 +08:00
log . ErrorWithSkip ( skip , "plainTextInternal (status=%d): write bytes failed: %v" , status , err )
2021-12-15 14:59:57 +08:00
}
2016-07-26 02:48:17 +08:00
}
2022-01-30 04:52:37 +08:00
// PlainTextBytes renders bytes as plain text
func ( ctx * Context ) PlainTextBytes ( status int , bs [ ] byte ) {
ctx . plainTextInternal ( 2 , status , bs )
}
2021-12-15 14:59:57 +08:00
// PlainText renders content as plain text
func ( ctx * Context ) PlainText ( status int , text string ) {
2022-01-30 04:52:37 +08:00
ctx . plainTextInternal ( 2 , status , [ ] byte ( text ) )
2021-01-26 23:36:53 +08:00
}
2021-12-15 14:59:57 +08:00
// RespHeader returns the response header
func ( ctx * Context ) RespHeader ( ) http . Header {
return ctx . Resp . Header ( )
2015-03-28 22:30:05 +08:00
}
2022-11-18 01:55:15 +08:00
type ServeHeaderOptions struct {
ContentType string // defaults to "application/octet-stream"
ContentTypeCharset string
2022-11-24 22:25:13 +08:00
ContentLength * int64
2022-11-18 01:55:15 +08:00
Disposition string // defaults to "attachment"
Filename string
CacheDuration time . Duration // defaults to 5 minutes
2022-11-24 22:25:13 +08:00
LastModified time . Time
2022-11-18 01:55:15 +08:00
}
2022-03-30 16:42:47 +08:00
// SetServeHeaders sets necessary content serve headers
2022-11-18 01:55:15 +08:00
func ( ctx * Context ) SetServeHeaders ( opts * ServeHeaderOptions ) {
header := ctx . Resp . Header ( )
contentType := typesniffer . ApplicationOctetStream
if opts . ContentType != "" {
if opts . ContentTypeCharset != "" {
contentType = opts . ContentType + "; charset=" + strings . ToLower ( opts . ContentTypeCharset )
} else {
contentType = opts . ContentType
}
}
header . Set ( "Content-Type" , contentType )
header . Set ( "X-Content-Type-Options" , "nosniff" )
2022-11-24 22:25:13 +08:00
if opts . ContentLength != nil {
header . Set ( "Content-Length" , strconv . FormatInt ( * opts . ContentLength , 10 ) )
}
2022-11-18 01:55:15 +08:00
if opts . Filename != "" {
disposition := opts . Disposition
if disposition == "" {
disposition = "attachment"
}
backslashEscapedName := strings . ReplaceAll ( strings . ReplaceAll ( opts . Filename , ` \ ` , ` \\ ` ) , ` " ` , ` \" ` ) // \ -> \\, " -> \"
header . Set ( "Content-Disposition" , fmt . Sprintf ( ` %s; filename="%s"; filename*=UTF-8''%s ` , disposition , backslashEscapedName , url . PathEscape ( opts . Filename ) ) )
header . Set ( "Access-Control-Expose-Headers" , "Content-Disposition" )
}
duration := opts . CacheDuration
if duration == 0 {
duration = 5 * time . Minute
}
2023-03-09 04:40:04 +08:00
httpcache . SetCacheControlInHeader ( header , duration )
2022-11-24 22:25:13 +08:00
if ! opts . LastModified . IsZero ( ) {
header . Set ( "Last-Modified" , opts . LastModified . UTC ( ) . Format ( http . TimeFormat ) )
}
2022-03-30 16:42:47 +08:00
}
2016-11-25 14:51:01 +08:00
// ServeContent serves content to http request
2022-11-24 22:25:13 +08:00
func ( ctx * Context ) ServeContent ( r io . ReadSeeker , opts * ServeHeaderOptions ) {
ctx . SetServeHeaders ( opts )
http . ServeContent ( ctx . Resp , ctx . Req , opts . Filename , opts . LastModified , r )
2021-01-26 23:36:53 +08:00
}
2022-03-30 16:42:47 +08:00
// UploadStream returns the request body or the first form file
// Only form files need to get closed.
func ( ctx * Context ) UploadStream ( ) ( rd io . ReadCloser , needToClose bool , err error ) {
contentType := strings . ToLower ( ctx . Req . Header . Get ( "Content-Type" ) )
if strings . HasPrefix ( contentType , "application/x-www-form-urlencoded" ) || strings . HasPrefix ( contentType , "multipart/form-data" ) {
if err := ctx . Req . ParseMultipartForm ( 32 << 20 ) ; err != nil {
return nil , false , err
}
if ctx . Req . MultipartForm . File == nil {
return nil , false , http . ErrMissingFile
}
for _ , files := range ctx . Req . MultipartForm . File {
if len ( files ) > 0 {
r , err := files [ 0 ] . Open ( )
return r , true , err
}
}
return nil , false , http . ErrMissingFile
}
return ctx . Req . Body , false , nil
}
2021-01-26 23:36:53 +08:00
// Error returned an error to web browser
func ( ctx * Context ) Error ( status int , contents ... string ) {
2022-01-21 01:46:10 +08:00
v := http . StatusText ( status )
2021-01-26 23:36:53 +08:00
if len ( contents ) > 0 {
v = contents [ 0 ]
}
http . Error ( ctx . Resp , v , status )
}
// JSON render content as JSON
func ( ctx * Context ) JSON ( status int , content interface { } ) {
2021-01-29 21:42:47 +08:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , "application/json;charset=utf-8" )
2021-01-28 05:46:22 +08:00
ctx . Resp . WriteHeader ( status )
2021-01-26 23:36:53 +08:00
if err := json . NewEncoder ( ctx . Resp ) . Encode ( content ) ; err != nil {
ctx . ServerError ( "Render JSON failed" , err )
}
}
2021-12-15 14:59:57 +08:00
// Redirect redirects the request
2021-01-26 23:36:53 +08:00
func ( ctx * Context ) Redirect ( location string , status ... int ) {
2022-03-23 12:54:07 +08:00
code := http . StatusSeeOther
2021-01-26 23:36:53 +08:00
if len ( status ) == 1 {
code = status [ 0 ]
}
http . Redirect ( ctx . Resp , ctx . Req , location , code )
}
2021-03-07 16:12:43 +08:00
// SetCookie convenience function to set most cookies consistently
// CSRF and a few others are the exception here
func ( ctx * Context ) SetCookie ( name , value string , expiry int ) {
middleware . SetCookie ( ctx . Resp , name , value ,
expiry ,
setting . AppSubURL ,
setting . SessionConfig . Domain ,
setting . SessionConfig . Secure ,
true ,
middleware . SameSite ( setting . SessionConfig . SameSite ) )
}
// DeleteCookie convenience function to delete most cookies consistently
// CSRF and a few others are the exception here
func ( ctx * Context ) DeleteCookie ( name string ) {
middleware . SetCookie ( ctx . Resp , name , "" ,
- 1 ,
setting . AppSubURL ,
setting . SessionConfig . Domain ,
setting . SessionConfig . Secure ,
true ,
middleware . SameSite ( setting . SessionConfig . SameSite ) )
2021-01-26 23:36:53 +08:00
}
// GetCookie returns given cookie value from request header.
func ( ctx * Context ) GetCookie ( name string ) string {
2021-01-30 16:55:53 +08:00
return middleware . GetCookie ( ctx . Req , name )
2021-01-26 23:36:53 +08:00
}
// GetSuperSecureCookie returns given cookie value from request header with secret string.
func ( ctx * Context ) GetSuperSecureCookie ( secret , name string ) ( string , bool ) {
val := ctx . GetCookie ( name )
2021-03-07 16:12:43 +08:00
return ctx . CookieDecrypt ( secret , val )
}
// CookieDecrypt returns given value from with secret string.
func ( ctx * Context ) CookieDecrypt ( secret , val string ) ( string , bool ) {
2021-01-26 23:36:53 +08:00
if val == "" {
return "" , false
}
text , err := hex . DecodeString ( val )
if err != nil {
return "" , false
}
key := pbkdf2 . Key ( [ ] byte ( secret ) , [ ] byte ( secret ) , 1000 , 16 , sha256 . New )
2022-04-01 16:47:50 +08:00
text , err = util . AESGCMDecrypt ( key , text )
2021-01-26 23:36:53 +08:00
return string ( text ) , err == nil
}
// SetSuperSecureCookie sets given cookie value to response header with secret string.
2021-03-07 16:12:43 +08:00
func ( ctx * Context ) SetSuperSecureCookie ( secret , name , value string , expiry int ) {
text := ctx . CookieEncrypt ( secret , value )
ctx . SetCookie ( name , text , expiry )
}
// CookieEncrypt encrypts a given value using the provided secret
func ( ctx * Context ) CookieEncrypt ( secret , value string ) string {
2021-01-26 23:36:53 +08:00
key := pbkdf2 . Key ( [ ] byte ( secret ) , [ ] byte ( secret ) , 1000 , 16 , sha256 . New )
2022-04-01 16:47:50 +08:00
text , err := util . AESGCMEncrypt ( key , [ ] byte ( value ) )
2021-01-26 23:36:53 +08:00
if err != nil {
panic ( "error encrypting cookie: " + err . Error ( ) )
}
2021-03-07 16:12:43 +08:00
return hex . EncodeToString ( text )
2021-01-26 23:36:53 +08:00
}
// GetCookieInt returns cookie result in int type.
func ( ctx * Context ) GetCookieInt ( name string ) int {
r , _ := strconv . Atoi ( ctx . GetCookie ( name ) )
return r
}
// GetCookieInt64 returns cookie result in int64 type.
func ( ctx * Context ) GetCookieInt64 ( name string ) int64 {
r , _ := strconv . ParseInt ( ctx . GetCookie ( name ) , 10 , 64 )
return r
}
// GetCookieFloat64 returns cookie result in float64 type.
func ( ctx * Context ) GetCookieFloat64 ( name string ) float64 {
v , _ := strconv . ParseFloat ( ctx . GetCookie ( name ) , 64 )
return v
}
// RemoteAddr returns the client machie ip address
func ( ctx * Context ) RemoteAddr ( ) string {
return ctx . Req . RemoteAddr
}
// Params returns the param on route
func ( ctx * Context ) Params ( p string ) string {
s , _ := url . PathUnescape ( chi . URLParam ( ctx . Req , strings . TrimPrefix ( p , ":" ) ) )
return s
}
// ParamsInt64 returns the param on route as int64
func ( ctx * Context ) ParamsInt64 ( p string ) int64 {
v , _ := strconv . ParseInt ( ctx . Params ( p ) , 10 , 64 )
return v
}
// SetParams set params into routes
func ( ctx * Context ) SetParams ( k , v string ) {
2021-05-31 14:18:11 +08:00
chiCtx := chi . RouteContext ( ctx )
2021-01-26 23:36:53 +08:00
chiCtx . URLParams . Add ( strings . TrimPrefix ( k , ":" ) , url . PathEscape ( v ) )
}
2021-12-15 14:59:57 +08:00
// Write writes data to web browser
2021-01-26 23:36:53 +08:00
func ( ctx * Context ) Write ( bs [ ] byte ) ( int , error ) {
return ctx . Resp . Write ( bs )
}
// Written returns true if there are something sent to web browser
func ( ctx * Context ) Written ( ) bool {
return ctx . Resp . Status ( ) > 0
}
// Status writes status code
func ( ctx * Context ) Status ( status int ) {
ctx . Resp . WriteHeader ( status )
}
2021-05-31 14:18:11 +08:00
// Deadline is part of the interface for context.Context and we pass this to the request context
func ( ctx * Context ) Deadline ( ) ( deadline time . Time , ok bool ) {
return ctx . Req . Context ( ) . Deadline ( )
}
// Done is part of the interface for context.Context and we pass this to the request context
func ( ctx * Context ) Done ( ) <- chan struct { } {
return ctx . Req . Context ( ) . Done ( )
}
// Err is part of the interface for context.Context and we pass this to the request context
func ( ctx * Context ) Err ( ) error {
return ctx . Req . Context ( ) . Err ( )
}
// Value is part of the interface for context.Context and we pass this to the request context
func ( ctx * Context ) Value ( key interface { } ) interface { } {
2022-01-20 07:26:57 +08:00
if key == git . RepositoryContextKey && ctx . Repo != nil {
return ctx . Repo . GitRepo
}
Append `(comment)` when a link points at a comment rather than the whole issue (#23734)
Close #23671
For the feature mentioned above, this PR append ' (comment)' to the
rendered html if it is a hashcomment.
After the PR, type in the following
```
pull request from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2
pull request from this repo:
http://localhost:3000/aaa/testA/pulls/2
issue comment from this repo:
http://localhost:3000/aaa/testA/issues/1#issuecomment-18
http://localhost:3000/aaa/testA/pulls/2#issue-9
issue comment from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issuecomment-24
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issue
```
Gives:
<img width="687" alt="截屏2023-03-27 13 53 06"
src="https://user-images.githubusercontent.com/17645053/227852387-2b218e0d-3468-4d90-ad81-d702ddd17fd2.png">
Other than the above feature, this PR also includes two other changes:
1 Right now, the render of links from file changed tab in pull request
might not be very proper, for example, if type in the following. (not
sure if this is an issue or design, if not an issue, I will revert the
changes). example on
[try.gitea.io](https://try.gitea.io/HesterG/testrepo/pulls/1)
```
https://try.gitea.io/HesterG/testrepo/pulls/1/files#issuecomment-162725
https://try.gitea.io/HesterG/testrepo/pulls/1/files
```
it will render the following
<img width="899" alt="截屏2023-03-24 15 41 37"
src="https://user-images.githubusercontent.com/17645053/227456117-5eccedb7-9118-4540-929d-aee9a76de852.png">
In this PR, skip processing the link into a ref issue if it is a link
from files changed tab in pull request
After:
type in following
```
hash comment on files changed tab:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24
files changed link:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files
```
Gives
<img width="708" alt="截屏2023-03-27 22 09 02"
src="https://user-images.githubusercontent.com/17645053/227964273-5dc06c50-3713-489c-b05d-d95367d0ab0f.png">
2 Right now, after editing the comment area, there will not be tippys
attached to `ref-issue`; and no tippy attached on preview as well.
example:
https://user-images.githubusercontent.com/17645053/227850540-5ae34e2d-b1d7-4d0d-9726-7701bf825d1f.mov
In this PR, in frontend, make sure tippy is added after editing the
comment, and to the comment on preview tab
After:
https://user-images.githubusercontent.com/17645053/227853777-06f56b4c-1148-467c-b6f7-f79418e67504.mov
2023-04-03 16:02:57 +08:00
if key == translation . ContextKey && ctx . Locale != nil {
return ctx . Locale
}
2021-05-31 14:18:11 +08:00
return ctx . Req . Context ( ) . Value ( key )
}
2022-04-08 02:59:56 +08:00
// SetTotalCountHeader set "X-Total-Count" header
func ( ctx * Context ) SetTotalCountHeader ( total int64 ) {
ctx . RespHeader ( ) . Set ( "X-Total-Count" , fmt . Sprint ( total ) )
ctx . AppendAccessControlExposeHeaders ( "X-Total-Count" )
}
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
func ( ctx * Context ) AppendAccessControlExposeHeaders ( names ... string ) {
val := ctx . RespHeader ( ) . Get ( "Access-Control-Expose-Headers" )
if len ( val ) != 0 {
ctx . RespHeader ( ) . Set ( "Access-Control-Expose-Headers" , fmt . Sprintf ( "%s, %s" , val , strings . Join ( names , ", " ) ) )
} else {
ctx . RespHeader ( ) . Set ( "Access-Control-Expose-Headers" , strings . Join ( names , ", " ) )
}
}
2021-01-26 23:36:53 +08:00
// Handler represents a custom handler
type Handler func ( * Context )
2021-12-15 14:59:57 +08:00
type contextKeyType struct { }
var contextKey interface { } = contextKeyType { }
2021-01-26 23:36:53 +08:00
// WithContext set up install context in request
func WithContext ( req * http . Request , ctx * Context ) * http . Request {
return req . WithContext ( context . WithValue ( req . Context ( ) , contextKey , ctx ) )
}
// GetContext retrieves install context from request
func GetContext ( req * http . Request ) * Context {
return req . Context ( ) . Value ( contextKey ) . ( * Context )
}
2021-09-28 21:13:04 +08:00
// GetContextUser returns context user
2021-11-24 17:49:20 +08:00
func GetContextUser ( req * http . Request ) * user_model . User {
2021-09-28 21:13:04 +08:00
if apiContext , ok := req . Context ( ) . Value ( apiContextKey ) . ( * APIContext ) ; ok {
2022-03-22 15:03:22 +08:00
return apiContext . Doer
2021-09-28 21:13:04 +08:00
}
if ctx , ok := req . Context ( ) . Value ( contextKey ) . ( * Context ) ; ok {
2022-03-22 15:03:22 +08:00
return ctx . Doer
2021-09-28 21:13:04 +08:00
}
return nil
}
2021-01-26 23:36:53 +08:00
func getCsrfOpts ( ) CsrfOptions {
return CsrfOptions {
Secret : setting . SecretKey ,
Cookie : setting . CSRFCookieName ,
SetCookie : true ,
Secure : setting . SessionConfig . Secure ,
CookieHTTPOnly : setting . CSRFCookieHTTPOnly ,
Header : "X-Csrf-Token" ,
CookieDomain : setting . SessionConfig . Domain ,
CookiePath : setting . SessionConfig . CookiePath ,
2021-03-07 16:12:43 +08:00
SameSite : setting . SessionConfig . SameSite ,
2021-01-26 23:36:53 +08:00
}
2014-04-11 02:37:43 +08:00
}
2014-07-26 12:24:27 +08:00
// Contexter initializes a classic context for a request.
2022-08-28 17:43:25 +08:00
func Contexter ( ctx context . Context ) func ( next http . Handler ) http . Handler {
_ , rnd := templates . HTMLRenderer ( ctx )
2022-01-21 01:46:10 +08:00
csrfOpts := getCsrfOpts ( )
2022-04-08 13:21:05 +08:00
if ! setting . IsProd {
CsrfTokenRegenerationInterval = 5 * time . Second // in dev, re-generate the tokens more aggressively for debug purpose
}
2021-01-26 23:36:53 +08:00
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( resp http . ResponseWriter , req * http . Request ) {
2022-01-21 01:46:10 +08:00
locale := middleware . Locale ( resp , req )
startTime := time . Now ( )
link := setting . AppSubURL + strings . TrimSuffix ( req . URL . EscapedPath ( ) , "/" )
2021-12-17 01:40:18 +08:00
2022-01-21 01:46:10 +08:00
ctx := Context {
2021-01-26 23:36:53 +08:00
Resp : NewResponse ( resp ) ,
2021-01-27 22:56:54 +08:00
Cache : mc . GetCache ( ) ,
2021-01-26 23:36:53 +08:00
Locale : locale ,
Link : link ,
Render : rnd ,
Session : session . GetSession ( req ) ,
Repo : & Repository {
PullRequest : & PullRequest { } ,
} ,
Org : & Organization { } ,
Data : map [ string ] interface { } {
"CurrentURL" : setting . AppSubURL + req . URL . RequestURI ( ) ,
"PageStartTime" : startTime ,
2021-01-29 12:33:47 +08:00
"Link" : link ,
2021-10-21 15:37:43 +08:00
"RunModeIsProd" : setting . IsProd ,
2021-01-26 23:36:53 +08:00
} ,
}
2022-05-05 22:13:23 +08:00
defer ctx . Close ( )
2021-10-15 10:35:26 +08:00
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
2021-10-13 02:11:35 +08:00
ctx . PageData = map [ string ] interface { } { }
ctx . Data [ "PageData" ] = ctx . PageData
2022-01-20 07:26:57 +08:00
ctx . Data [ "Context" ] = & ctx
2021-01-26 23:36:53 +08:00
ctx . Req = WithContext ( req , & ctx )
2022-04-08 13:21:05 +08:00
ctx . csrf = PrepareCSRFProtector ( csrfOpts , & ctx )
2021-01-26 23:36:53 +08:00
// Get flash.
flashCookie := ctx . GetCookie ( "macaron_flash" )
vals , _ := url . ParseQuery ( flashCookie )
if len ( vals ) > 0 {
2021-01-30 16:55:53 +08:00
f := & middleware . Flash {
2021-01-26 23:36:53 +08:00
DataStore : & ctx ,
Values : vals ,
ErrorMsg : vals . Get ( "error" ) ,
SuccessMsg : vals . Get ( "success" ) ,
InfoMsg : vals . Get ( "info" ) ,
WarningMsg : vals . Get ( "warning" ) ,
}
ctx . Data [ "Flash" ] = f
}
2021-01-30 16:55:53 +08:00
f := & middleware . Flash {
2021-01-26 23:36:53 +08:00
DataStore : & ctx ,
Values : url . Values { } ,
ErrorMsg : "" ,
WarningMsg : "" ,
InfoMsg : "" ,
SuccessMsg : "" ,
}
ctx . Resp . Before ( func ( resp ResponseWriter ) {
if flash := f . Encode ( ) ; len ( flash ) > 0 {
2021-01-30 16:55:53 +08:00
middleware . SetCookie ( resp , "macaron_flash" , flash , 0 ,
2021-01-27 22:56:54 +08:00
setting . SessionConfig . CookiePath ,
2021-01-30 16:55:53 +08:00
middleware . Domain ( setting . SessionConfig . Domain ) ,
middleware . HTTPOnly ( true ) ,
middleware . Secure ( setting . SessionConfig . Secure ) ,
2021-03-07 16:12:43 +08:00
middleware . SameSite ( setting . SessionConfig . SameSite ) ,
2021-01-27 22:56:54 +08:00
)
return
2021-01-26 23:36:53 +08:00
}
2021-03-07 16:12:43 +08:00
middleware . SetCookie ( ctx . Resp , "macaron_flash" , "" , - 1 ,
2021-01-26 23:36:53 +08:00
setting . SessionConfig . CookiePath ,
2021-01-30 16:55:53 +08:00
middleware . Domain ( setting . SessionConfig . Domain ) ,
middleware . HTTPOnly ( true ) ,
middleware . Secure ( setting . SessionConfig . Secure ) ,
2021-03-07 16:12:43 +08:00
middleware . SameSite ( setting . SessionConfig . SameSite ) ,
2021-01-26 23:36:53 +08:00
)
} )
ctx . Flash = f
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
if ctx . Req . Method == "POST" && strings . Contains ( ctx . Req . Header . Get ( "Content-Type" ) , "multipart/form-data" ) {
if err := ctx . Req . ParseMultipartForm ( setting . Attachment . MaxSize << 20 ) ; err != nil && ! strings . Contains ( err . Error ( ) , "EOF" ) { // 32MB max size
ctx . ServerError ( "ParseMultipartForm" , err )
return
}
}
2018-08-27 10:23:27 +08:00
2023-03-09 04:40:04 +08:00
httpcache . SetCacheControlInHeader ( ctx . Resp . Header ( ) , 0 , "no-transform" )
2021-08-07 04:47:10 +08:00
ctx . Resp . Header ( ) . Set ( ` X-Frame-Options ` , setting . CORSConfig . XFrameOptions )
2021-01-26 23:36:53 +08:00
2022-04-08 13:21:05 +08:00
ctx . Data [ "CsrfToken" ] = ctx . csrf . GetToken ( )
2021-01-26 23:36:53 +08:00
ctx . Data [ "CsrfTokenHtml" ] = template . HTML ( ` <input type="hidden" name="_csrf" value=" ` + ctx . Data [ "CsrfToken" ] . ( string ) + ` "> ` )
2021-05-05 05:48:31 +08:00
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
2021-01-26 23:36:53 +08:00
ctx . Data [ "IsLandingPageHome" ] = setting . LandingPageURL == setting . LandingPageHome
ctx . Data [ "IsLandingPageExplore" ] = setting . LandingPageURL == setting . LandingPageExplore
ctx . Data [ "IsLandingPageOrganizations" ] = setting . LandingPageURL == setting . LandingPageOrganizations
ctx . Data [ "ShowRegistrationButton" ] = setting . Service . ShowRegistrationButton
ctx . Data [ "ShowMilestonesDashboardPage" ] = setting . Service . ShowMilestonesDashboardPage
ctx . Data [ "ShowFooterBranding" ] = setting . ShowFooterBranding
ctx . Data [ "ShowFooterVersion" ] = setting . ShowFooterVersion
ctx . Data [ "EnableSwagger" ] = setting . API . EnableSwagger
ctx . Data [ "EnableOpenIDSignIn" ] = setting . Service . EnableOpenIDSignIn
ctx . Data [ "DisableMigrations" ] = setting . Repository . DisableMigrations
2021-04-16 00:53:57 +08:00
ctx . Data [ "DisableStars" ] = setting . Repository . DisableStars
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 09:45:19 +08:00
ctx . Data [ "EnableActions" ] = setting . Actions . Enabled
2021-01-26 23:36:53 +08:00
ctx . Data [ "ManifestData" ] = setting . ManifestData
2021-11-10 03:57:58 +08:00
ctx . Data [ "UnitWikiGlobalDisabled" ] = unit . TypeWiki . UnitGlobalDisabled ( )
ctx . Data [ "UnitIssuesGlobalDisabled" ] = unit . TypeIssues . UnitGlobalDisabled ( )
ctx . Data [ "UnitPullsGlobalDisabled" ] = unit . TypePullRequests . UnitGlobalDisabled ( )
ctx . Data [ "UnitProjectsGlobalDisabled" ] = unit . TypeProjects . UnitGlobalDisabled ( )
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 09:45:19 +08:00
ctx . Data [ "UnitActionsGlobalDisabled" ] = unit . TypeActions . UnitGlobalDisabled ( )
2021-05-05 05:48:31 +08:00
2022-06-28 04:58:46 +08:00
ctx . Data [ "locale" ] = locale
2021-01-26 23:36:53 +08:00
ctx . Data [ "AllLangs" ] = translation . AllLangs ( )
2020-12-22 19:13:50 +08:00
2021-01-26 23:36:53 +08:00
next . ServeHTTP ( ctx . Resp , ctx . Req )
2021-08-05 01:26:30 +08:00
// Handle adding signedUserName to the context for the AccessLogger
usernameInterface := ctx . Data [ "SignedUserName" ]
identityPtrInterface := ctx . Req . Context ( ) . Value ( signedUserNameStringPointerKey )
if usernameInterface != nil && identityPtrInterface != nil {
username := usernameInterface . ( string )
identityPtr := identityPtrInterface . ( * string )
if identityPtr != nil && username != "" {
* identityPtr = username
}
}
2021-01-26 23:36:53 +08:00
} )
2014-03-15 19:01:50 +08:00
}
}
2022-04-08 02:59:56 +08:00
// SearchOrderByMap represents all possible search order
var SearchOrderByMap = map [ string ] map [ string ] db . SearchOrderBy {
"asc" : {
"alpha" : db . SearchOrderByAlphabetically ,
"created" : db . SearchOrderByOldest ,
"updated" : db . SearchOrderByLeastUpdated ,
"size" : db . SearchOrderBySize ,
"id" : db . SearchOrderByID ,
} ,
"desc" : {
"alpha" : db . SearchOrderByAlphabeticallyReverse ,
"created" : db . SearchOrderByNewest ,
"updated" : db . SearchOrderByRecentUpdated ,
"size" : db . SearchOrderBySizeReverse ,
"id" : db . SearchOrderByIDReverse ,
} ,
}