davrot Mods

This commit is contained in:
David Rotermund 2025-02-09 00:45:35 +01:00
parent f3862b1c33
commit 25bd68b747
15 changed files with 234 additions and 72 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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 {

View file

@ -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)

View file

@ -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,

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -11,41 +11,10 @@
</div>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.install_desc" "https://forgejo.org/download/#installation-from-binary" "https://forgejo.org/download/#container-image" "https://forgejo.org/download"}}
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.platform_desc"}}
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.lightweight_desc"}}
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.license_desc" "https://forgejo.org/download" "https://codeberg.org/forgejo/forgejo"}}
</p>
</div>
</div>
{{if .LandingPageInfoEnabled}}
{{template "landing-page" .}}
{{end}}
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,36 @@
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.install_desc" "https://forgejo.org/download/#installation-from-binary" "https://forgejo.org/download/#container-image" "https://forgejo.org/download"}}
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.platform_desc"}}
</p>
</div>
</div>
<div class="ui stackable middle very relaxed page grid">
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.lightweight_desc"}}
</p>
</div>
<div class="eight wide center column">
<h1 class="hero ui icon header">
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
</h1>
<p class="large">
{{ctx.Locale.Tr "startpage.license_desc" "https://forgejo.org/download" "https://codeberg.org/forgejo/forgejo"}}
</p>
</div>
</div>

View file

@ -114,6 +114,15 @@
{{- end -}}
</span>
{{end}}
{{if (not .IsViewFile)}}
{{if and .CanDeleteFile (not .IsBlame)}}
<a href="{{.RepoLink}}/_delete_path/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}"><span class="btn-octicon btn-octicon-danger" data-tooltip-content="{{.DeleteFileTooltip}}">{{svg "octicon-trash"}}</span></a>
{{else}}
<span class="btn-octicon disabled" data-tooltip-content="{{.DeleteFileTooltip}}">{{svg "octicon-trash"}}</span>
{{end}}
{{end}}
</div>
<div class="tw-flex tw-items-center">
<!-- Only show clone panel in repository home page -->

View file

@ -50,18 +50,21 @@
</div>
</div>
<div class="ui container fluid">
{{template "user/auth/webauthn_error" .}}
{{if .SignInForgottenPasswordEnabled}}
<div class="ui container fluid">
{{template "user/auth/webauthn_error" .}}
<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
{{if .ShowRegistrationButton}}
<div class="field">
{{ctx.Locale.Tr "auth.hint_register" (printf "%s/user/sign_up" AppSubUrl)}}
<br>
<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
{{if .ShowRegistrationButton}}
<div class="field">
{{ctx.Locale.Tr "auth.hint_register" (printf "%s/user/sign_up" AppSubUrl)}}
<br>
</div>
{{end}}
<div class="field">
<a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
</div>
</div>
{{end}}
<div class="field">
<a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -1,19 +1,19 @@
import $ from 'jquery';
import '../vendor/jquery.are-you-sure.js';
import {clippie} from 'clippie';
import {createDropzone} from './dropzone.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js';
import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
import {showTemporaryTooltip} from '../modules/tippy.js';
import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js';
import {request, POST, GET} from '../modules/fetch.js';
import { clippie } from 'clippie';
import { createDropzone } from './dropzone.js';
import { showGlobalErrorMessage } from '../bootstrap.js';
import { handleGlobalEnterQuickSubmit } from './comp/QuickSubmit.js';
import { svg } from '../svg.js';
import { hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter } from '../utils/dom.js';
import { htmlEscape } from 'escape-goat';
import { showTemporaryTooltip } from '../modules/tippy.js';
import { confirmModal } from './comp/ConfirmModal.js';
import { showErrorToast } from '../modules/toast.js';
import { request, POST, GET } from '../modules/fetch.js';
import '../htmx.js';
const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
const { appUrl, appSubUrl, csrfToken, i18n } = window.config;
export function initGlobalFormDirtyLeaveConfirm() {
// Warn users that try to leave a page after entering data into a form.
@ -82,7 +82,7 @@ async function fetchActionDoRequest(actionElem, url, opt) {
try {
const resp = await request(url, opt);
if (resp.status === 200) {
let {redirect} = await resp.json();
let { redirect } = await resp.json();
redirect = redirect || actionElem.getAttribute('data-redirect');
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
if (redirect) {
@ -96,7 +96,7 @@ async function fetchActionDoRequest(actionElem, url, opt) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
if (data.errorMessage) {
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
showErrorToast(data.errorMessage, { useHtmlBody: data.renderFormat === 'html' });
} else {
showErrorToast(`server error: ${resp.status}`);
}
@ -134,7 +134,7 @@ async function formFetchAction(e) {
}
let reqUrl = formActionUrl;
const reqOpt = {method: formMethod.toUpperCase()};
const reqOpt = { method: formMethod.toUpperCase() };
if (formMethod.toLowerCase() === 'get') {
const params = new URLSearchParams();
for (const [key, value] of formData) {
@ -212,7 +212,7 @@ export function initDropzone(el) {
const $dropzone = $(el);
const _promise = createDropzone(el, {
url: $dropzone.data('upload-url'),
headers: {'X-Csrf-Token': csrfToken},
headers: { 'X-Csrf-Token': csrfToken },
maxFiles: $dropzone.data('max-file'),
maxFilesize: $dropzone.data('max-size'),
acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
@ -229,7 +229,8 @@ export function initDropzone(el) {
this.on('success', (file, data) => {
file.uuid = data.uuid;
const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append($input);
const $inputPath = $(`<input type="hidden" name="files_fullpath[${data.uuid}]" value="${htmlEscape(file.fullPath || file.name)}">`);
$dropzone.find('.files').append($input).append($inputPath);
// Create a "Copy Link" element, to conveniently copy the image
// or file link as Markdown to the clipboard
const copyLinkElement = document.createElement('div');
@ -250,10 +251,15 @@ export function initDropzone(el) {
file.previewTemplate.append(copyLinkElement);
});
this.on('removedfile', (file) => {
// Remove the hidden input for the file
$(`#${file.uuid}`).remove();
// Remove the hidden input for files_fullpath
$(`input[name="files_fullpath[${file.uuid}]"]`).remove();
if ($dropzone.data('remove-url')) {
POST($dropzone.data('remove-url'), {
data: new URLSearchParams({file: file.uuid}),
data: new URLSearchParams({ file: file.uuid }),
});
}
});
@ -276,7 +282,7 @@ async function linkAction(e) {
const url = el.getAttribute('data-url');
const doRequest = async () => {
el.disabled = true;
await fetchActionDoRequest(el, url, {method: 'POST'});
await fetchActionDoRequest(el, url, { method: 'POST' });
el.disabled = false;
};
@ -287,7 +293,7 @@ async function linkAction(e) {
}
const isRisky = el.classList.contains('red') || el.classList.contains('yellow') || el.classList.contains('orange') || el.classList.contains('negative');
if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'primary'})) {
if (await confirmModal({ content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'primary' })) {
await doRequest();
}
}
@ -331,7 +337,7 @@ export function initGlobalLinkActions() {
}
}
const response = await POST($this.data('url'), {data: postData});
const response = await POST($this.data('url'), { data: postData });
if (response.ok) {
const data = await response.json();
window.location.href = data.redirect;