diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index f45b6e6..d7a3790 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -6,6 +6,7 @@ package git import ( "bytes" "context" + "errors" "os" "path/filepath" "strings" @@ -102,6 +103,30 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { return filelist, err } +// Gives a list of all files in a directory and below +func (repo *Repository) LsFilesFromDirectory(directory, branch string) ([]string, error) { + if branch == "" { + return nil, errors.New("branch not found in context URL") + } + + cmd := NewCommand(repo.Ctx, "ls-files").AddDynamicArguments("--with-tree="+branch) + if len(directory) > 0 { + cmd = NewCommand(repo.Ctx, "ls-files").AddDynamicArguments("--with-tree="+branch).AddDynamicArguments("--directory").AddDynamicArguments(directory) + } + res, stderror, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + if len(stderror) > 0 { + return nil, errors.New(string(stderror)) + } + + lines := strings.Split(string(res), "\n") + + return lines, nil +} + // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present. func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { objectFormat, err := repo.GetObjectFormat() diff --git a/modules/setting/service.go b/modules/setting/service.go index 74ed5cd..b877fbd 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -85,6 +85,8 @@ var Service = struct { DefaultOrgMemberVisible bool UserDeleteWithCommentsMaxTime time.Duration ValidSiteURLSchemes []string + LandingPageInfoEnabled bool + SignInForgottenPasswordEnabled bool // OpenID settings EnableOpenIDSignIn bool @@ -213,6 +215,8 @@ func loadServiceFrom(rootCfg ConfigProvider) { if Service.EnableTimetracking { Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true) } + Service.LandingPageInfoEnabled = sec.Key("ENABLE_LANDING_PAGE_INFO").MustBool(true) + Service.SignInForgottenPasswordEnabled = sec.Key("SIGNIN_FORGOTTEN_PASSWORD_ENABLED").MustBool(true) Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true) Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true) Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index ccab47a..968f596 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -169,6 +169,8 @@ func SignIn(ctx *context.Context) { if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { context.SetCaptchaData(ctx) } + + ctx.Data["SignInForgottenPasswordEnabled"] = setting.Service.SignInForgottenPasswordEnabled ctx.HTML(http.StatusOK, tplSignIn) } @@ -189,6 +191,7 @@ func SignInPost(ctx *context.Context) { ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn + ctx.Data["SignInForgottenPasswordEnabled"] = setting.Service.SignInForgottenPasswordEnabled // Permission denied if EnableInternalSignIn is false if !setting.Service.EnableInternalSignIn { diff --git a/routers/web/home.go b/routers/web/home.go index d4be093..3fa2524 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -59,6 +59,8 @@ func Home(ctx *context.Context) { return } + ctx.Data["LandingPageInfoEnabled"] = setting.Service.LandingPageInfoEnabled + ctx.Data["PageIsHome"] = true ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.HTML(http.StatusOK, tplHome) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index f27ad62..2a43039 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -489,8 +489,23 @@ func DeleteFile(ctx *context.Context) { ctx.HTML(http.StatusOK, tplDeleteFile) } +// DeletePath render delete file page +func DeletePath(ctx *context.Context) { + DeleteFile(ctx) +} + // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { + DeletePathOrFilePost(ctx, false) +} + +// DeletePathPost response for deleting path +func DeletePathPost(ctx *context.Context) { + DeletePathOrFilePost(ctx, true) +} + +// DeletePathOrFilePost response for deleting path or file +func DeletePathOrFilePost(ctx *context.Context, isdir bool) { form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) canCommit := renderCommitRights(ctx) branchName := ctx.Repo.BranchName @@ -543,6 +558,7 @@ func DeleteFilePost(ctx *context.Context) { TreePath: ctx.Repo.TreePath, }, }, + IsDir: isdir, // Add this flag to indicate directory deletion Message: message, Signoff: form.Signoff, Author: gitIdentity, @@ -758,6 +774,7 @@ func UploadFilePost(ctx *context.Context) { TreePath: form.TreePath, Message: message, Files: form.Files, + FullPaths: form.FullPaths, Signoff: form.Signoff, Author: gitIdentity, Committer: gitIdentity, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 6329f5d..a3976ec 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -1219,6 +1219,27 @@ PostRecentBranchCheck: } else { ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions } + + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("git_model.GetTreePathLock", err) + return + } + + if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + ctx.Data["CanDeleteFile"] = false + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") + } else { + ctx.Data["CanDeleteFile"] = true + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") + } + } else if !ctx.Repo.IsViewBranch { + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") + } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") + } + isAnnexFile, okAnnexFile := ctx.Data["IsAnnexFile"] isAnnexFilePresent, okAnnexFilePresent := ctx.Data["IsAnnexFilePresent"] if okAnnexFile && okAnnexFilePresent && isAnnexFile.(bool) && !isAnnexFilePresent.(bool) { diff --git a/routers/web/web.go b/routers/web/web.go index db0015f..a276fa2 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -51,6 +51,7 @@ import ( _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters + "code.forgejo.org/go-chi/binding" "code.forgejo.org/go-chi/captcha" chi_middleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" @@ -1268,11 +1269,13 @@ func registerRoutes(m *web.Route) { m.Combo("/_new/*").Get(repo.NewFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) + m.Combo("/_delete_path/*").Get(repo.DeletePath). + Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeletePathPost) m.Combo("/_delete/*").Get(repo.DeleteFile). Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) m.Combo("/_upload/*", repo.MustBeAbleToUpload). Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) + Post(BindUpload(forms.UploadRepoFileForm{}), repo.UploadFilePost) m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick). @@ -1694,3 +1697,20 @@ func registerRoutes(m *web.Route) { ctx.NotFound("", nil) }) } + +func BindUpload(f forms.UploadRepoFileForm) http.HandlerFunc { + return func(resp http.ResponseWriter, req *http.Request) { + theObj := new(forms.UploadRepoFileForm) // create a new form obj for every request but not use obj directly + data := middleware.GetContextData(req.Context()) + binding.Bind(req, theObj) + files := theObj.Files + var fullpaths []string + for _, fileID := range files { + fullPath := req.Form.Get("files_fullpath[" + fileID + "]") + fullpaths = append(fullpaths, fullPath) + } + theObj.FullPaths = fullpaths + data.GetData()["__form"] = theObj + middleware.AssignForm(theObj, data) + } +} diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1ce9b29..75936f8 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -671,6 +671,7 @@ type UploadRepoFileForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` Files []string + FullPaths []string CommitMailID int64 `binding:"Required"` Signoff bool } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index d6025b6..2622387 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -5,6 +5,7 @@ package files import ( "context" + "errors" "fmt" "io" "path" @@ -56,6 +57,7 @@ type ChangeRepoFilesOptions struct { Committer *IdentityOptions Dates *CommitDateOptions Signoff bool + IsDir bool `default:"false"` } type RepoFileOptions struct { @@ -90,6 +92,29 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return nil, err } + if opts.IsDir { + var new_opts_files []*ChangeRepoFile + for _, file := range opts.Files { + if file.Operation != "delete" { + return nil, errors.New("invalid operation: only delete is allowed for directory paths") + } + treePath := CleanUploadFileName(file.TreePath) + filelist, err := gitRepo.LsFilesFromDirectory(treePath, opts.OldBranch) + if err != nil { + return nil, err + } + for _, filename := range filelist { + if len(filename) > 0 { + new_opts_files = append(new_opts_files, &ChangeRepoFile{ + Operation: "delete", + TreePath: filename, + }) + } + } + } + opts.Files = new_opts_files + } + var treePaths []string for _, file := range opts.Files { // If FromTreePath is not set, set it to the opts.TreePath diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index 21cd5a8..18b67ea 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -6,10 +6,12 @@ package files import ( "context" "fmt" + "html" "io" "os" "path" "path/filepath" + "regexp" "strings" git_model "code.gitea.io/gitea/models/git" @@ -30,7 +32,8 @@ type UploadRepoFileOptions struct { Message string Author *IdentityOptions Committer *IdentityOptions - Files []string // In UUID format. + Files []string // In UUID format + FullPaths []string Signoff bool } @@ -59,6 +62,10 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return nil } + if len(opts.Files) != len(opts.FullPaths) { + return nil + } + uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files) if err != nil { return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err) @@ -68,6 +75,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use infos := make([]uploadInfo, len(uploads)) for i, upload := range uploads { // Check file is not lfs locked, will return nil if lock setting not enabled + upload.Name = fileNameSanitize(html.UnescapeString(opts.FullPaths[i])) filepath := path.Join(opts.TreePath, upload.Name) lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath) if err != nil { @@ -178,6 +186,19 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return repo_model.DeleteUploads(ctx, uploads...) } +// From forgejo/services/repository/generate.go (but now allows /) +var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) + +// Sanitize user input to valid OS filenames +// +// Based on https://github.com/sindresorhus/filename-reserved-regex +// Adds ".." to prevent directory traversal +func fileNameSanitize(s string) string { + + // Added this because I am not sure what Windows will deliver us \ or / but we need /. + s = strings.ReplaceAll(s, "\\", "/") + return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_")) +} + func copyUploadedLFSFilesIntoRepository(infos []uploadInfo, t *TemporaryUploadRepository, treePath string) error { var storeInLFSFunc func(string) (bool, error) diff --git a/templates/home.tmpl b/templates/home.tmpl index a974344..5f84fca 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -11,41 +11,10 @@ -
- {{ctx.Locale.Tr "startpage.install_desc" "https://forgejo.org/download/#installation-from-binary" "https://forgejo.org/download/#container-image" "https://forgejo.org/download"}} -
-- {{ctx.Locale.Tr "startpage.platform_desc"}} -
-- {{ctx.Locale.Tr "startpage.lightweight_desc"}} -
-- {{ctx.Locale.Tr "startpage.license_desc" "https://forgejo.org/download" "https://codeberg.org/forgejo/forgejo"}} -
-+ {{ctx.Locale.Tr "startpage.install_desc" "https://forgejo.org/download/#installation-from-binary" "https://forgejo.org/download/#container-image" "https://forgejo.org/download"}} +
++ {{ctx.Locale.Tr "startpage.platform_desc"}} +
++ {{ctx.Locale.Tr "startpage.lightweight_desc"}} +
++ {{ctx.Locale.Tr "startpage.license_desc" "https://forgejo.org/download" "https://codeberg.org/forgejo/forgejo"}} +
+