mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 12:53:43 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			314 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			314 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package repo
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"image"
 | 
						|
	"io"
 | 
						|
	"path"
 | 
						|
	"slices"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	git_model "code.gitea.io/gitea/models/git"
 | 
						|
	issue_model "code.gitea.io/gitea/models/issues"
 | 
						|
	"code.gitea.io/gitea/models/renderhelper"
 | 
						|
	user_model "code.gitea.io/gitea/models/user"
 | 
						|
	"code.gitea.io/gitea/modules/actions"
 | 
						|
	"code.gitea.io/gitea/modules/charset"
 | 
						|
	"code.gitea.io/gitea/modules/git"
 | 
						|
	"code.gitea.io/gitea/modules/highlight"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/markup"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	"code.gitea.io/gitea/services/context"
 | 
						|
	issue_service "code.gitea.io/gitea/services/issue"
 | 
						|
	files_service "code.gitea.io/gitea/services/repository/files"
 | 
						|
 | 
						|
	"github.com/nektos/act/pkg/model"
 | 
						|
)
 | 
						|
 | 
						|
func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
 | 
						|
	ctx.Data["IsViewFile"] = true
 | 
						|
	ctx.Data["HideRepoInfo"] = true
 | 
						|
	blob := entry.Blob()
 | 
						|
	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("getFileReader", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer dataRc.Close()
 | 
						|
 | 
						|
	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
 | 
						|
	ctx.Data["FileIsSymlink"] = entry.IsLink()
 | 
						|
	ctx.Data["FileName"] = blob.Name()
 | 
						|
	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
 | 
						|
 | 
						|
	commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("GetCommitByPath", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !loadLatestCommitData(ctx, commit) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if ctx.Repo.TreePath == ".editorconfig" {
 | 
						|
		_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
 | 
						|
		if editorconfigWarning != nil {
 | 
						|
			ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
 | 
						|
		}
 | 
						|
		if editorconfigErr != nil {
 | 
						|
			ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
 | 
						|
		}
 | 
						|
	} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
 | 
						|
		_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
 | 
						|
		if issueConfigErr != nil {
 | 
						|
			ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
 | 
						|
		}
 | 
						|
	} else if actions.IsWorkflow(ctx.Repo.TreePath) {
 | 
						|
		content, err := actions.GetContentFromEntry(entry)
 | 
						|
		if err != nil {
 | 
						|
			log.Error("actions.GetContentFromEntry: %v", err)
 | 
						|
		}
 | 
						|
		_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
 | 
						|
		if workFlowErr != nil {
 | 
						|
			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
 | 
						|
		}
 | 
						|
	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
 | 
						|
		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
 | 
						|
			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
 | 
						|
			if len(warnings) > 0 {
 | 
						|
				ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	isDisplayingSource := ctx.FormString("display") == "source"
 | 
						|
	isDisplayingRendered := !isDisplayingSource
 | 
						|
 | 
						|
	if fInfo.isLFSFile {
 | 
						|
		ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
 | 
						|
	}
 | 
						|
 | 
						|
	isRepresentableAsText := fInfo.st.IsRepresentableAsText()
 | 
						|
	if !isRepresentableAsText {
 | 
						|
		// If we can't show plain text, always try to render.
 | 
						|
		isDisplayingSource = false
 | 
						|
		isDisplayingRendered = true
 | 
						|
	}
 | 
						|
	ctx.Data["IsLFSFile"] = fInfo.isLFSFile
 | 
						|
	ctx.Data["FileSize"] = fInfo.fileSize
 | 
						|
	ctx.Data["IsTextFile"] = fInfo.isTextFile
 | 
						|
	ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
 | 
						|
	ctx.Data["IsDisplayingSource"] = isDisplayingSource
 | 
						|
	ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
 | 
						|
	ctx.Data["IsExecutable"] = entry.IsExecutable()
 | 
						|
 | 
						|
	isTextSource := fInfo.isTextFile || isDisplayingSource
 | 
						|
	ctx.Data["IsTextSource"] = isTextSource
 | 
						|
	if isTextSource {
 | 
						|
		ctx.Data["CanCopyContent"] = true
 | 
						|
	}
 | 
						|
 | 
						|
	// Check LFS Lock
 | 
						|
	lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
 | 
						|
	ctx.Data["LFSLock"] = lfsLock
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("GetTreePathLock", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if lfsLock != nil {
 | 
						|
		u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetTreePathLock", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		ctx.Data["LFSLockOwner"] = u.Name
 | 
						|
		ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
 | 
						|
		ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
 | 
						|
	}
 | 
						|
 | 
						|
	// Assume file is not editable first.
 | 
						|
	if fInfo.isLFSFile {
 | 
						|
		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
 | 
						|
	} else if !isRepresentableAsText {
 | 
						|
		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
 | 
						|
	}
 | 
						|
 | 
						|
	switch {
 | 
						|
	case isRepresentableAsText:
 | 
						|
		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
 | 
						|
			ctx.Data["IsFileTooLarge"] = true
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		if fInfo.st.IsSvgImage() {
 | 
						|
			ctx.Data["IsImageFile"] = true
 | 
						|
			ctx.Data["CanCopyContent"] = true
 | 
						|
			ctx.Data["HasSourceRenderedToggle"] = true
 | 
						|
		}
 | 
						|
 | 
						|
		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 | 
						|
 | 
						|
		shouldRenderSource := ctx.FormString("display") == "source"
 | 
						|
		readmeExist := util.IsReadmeFileName(blob.Name())
 | 
						|
		ctx.Data["ReadmeExist"] = readmeExist
 | 
						|
 | 
						|
		markupType := markup.DetectMarkupTypeByFileName(blob.Name())
 | 
						|
		if markupType == "" {
 | 
						|
			markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
 | 
						|
		}
 | 
						|
		if markupType != "" {
 | 
						|
			ctx.Data["HasSourceRenderedToggle"] = true
 | 
						|
		}
 | 
						|
		if markupType != "" && !shouldRenderSource {
 | 
						|
			ctx.Data["IsMarkup"] = true
 | 
						|
			ctx.Data["MarkupType"] = markupType
 | 
						|
			metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
 | 
						|
			metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
 | 
						|
			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
 | 
						|
				CurrentRefPath:  ctx.Repo.RefTypeNameSubURL(),
 | 
						|
				CurrentTreePath: path.Dir(ctx.Repo.TreePath),
 | 
						|
			}).
 | 
						|
				WithMarkupType(markupType).
 | 
						|
				WithRelativePath(ctx.Repo.TreePath).
 | 
						|
				WithMetas(metas)
 | 
						|
 | 
						|
			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("Render", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			// to prevent iframe load third-party url
 | 
						|
			ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
 | 
						|
		} else {
 | 
						|
			buf, _ := io.ReadAll(rd)
 | 
						|
 | 
						|
			// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
 | 
						|
			//   empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
 | 
						|
			// Gitea uses the definition (like most modern editors):
 | 
						|
			//   empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
 | 
						|
			//   When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
 | 
						|
			//   To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
 | 
						|
			// This NumLines is only used for the display on the UI: "xxx lines"
 | 
						|
			if len(buf) == 0 {
 | 
						|
				ctx.Data["NumLines"] = 0
 | 
						|
			} else {
 | 
						|
				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
 | 
						|
			}
 | 
						|
 | 
						|
			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
 | 
						|
			if err != nil {
 | 
						|
				log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
 | 
						|
			}
 | 
						|
 | 
						|
			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
 | 
						|
			ctx.Data["LexerName"] = lexerName
 | 
						|
			if err != nil {
 | 
						|
				log.Error("highlight.File failed, fallback to plain text: %v", err)
 | 
						|
				fileContent = highlight.PlainText(buf)
 | 
						|
			}
 | 
						|
			status := &charset.EscapeStatus{}
 | 
						|
			statuses := make([]*charset.EscapeStatus, len(fileContent))
 | 
						|
			for i, line := range fileContent {
 | 
						|
				statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
 | 
						|
				status = status.Or(statuses[i])
 | 
						|
			}
 | 
						|
			ctx.Data["EscapeStatus"] = status
 | 
						|
			ctx.Data["FileContent"] = fileContent
 | 
						|
			ctx.Data["LineEscapeStatus"] = statuses
 | 
						|
		}
 | 
						|
		if !fInfo.isLFSFile {
 | 
						|
			if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
 | 
						|
				if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
 | 
						|
					ctx.Data["CanEditFile"] = false
 | 
						|
					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
 | 
						|
				} else {
 | 
						|
					ctx.Data["CanEditFile"] = true
 | 
						|
					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
 | 
						|
				}
 | 
						|
			} else if !ctx.Repo.RefFullName.IsBranch() {
 | 
						|
				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
 | 
						|
			} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
 | 
						|
				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
	case fInfo.st.IsPDF():
 | 
						|
		ctx.Data["IsPDFFile"] = true
 | 
						|
	case fInfo.st.IsVideo():
 | 
						|
		ctx.Data["IsVideoFile"] = true
 | 
						|
	case fInfo.st.IsAudio():
 | 
						|
		ctx.Data["IsAudioFile"] = true
 | 
						|
	case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
 | 
						|
		ctx.Data["IsImageFile"] = true
 | 
						|
		ctx.Data["CanCopyContent"] = true
 | 
						|
	default:
 | 
						|
		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
 | 
						|
			ctx.Data["IsFileTooLarge"] = true
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
 | 
						|
		// It is used by "external renders", markupRender will execute external programs to get rendered content.
 | 
						|
		if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
 | 
						|
			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 | 
						|
			ctx.Data["IsMarkup"] = true
 | 
						|
			ctx.Data["MarkupType"] = markupType
 | 
						|
 | 
						|
			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
 | 
						|
				CurrentRefPath:  ctx.Repo.RefTypeNameSubURL(),
 | 
						|
				CurrentTreePath: path.Dir(ctx.Repo.TreePath),
 | 
						|
			}).
 | 
						|
				WithMarkupType(markupType).
 | 
						|
				WithRelativePath(ctx.Repo.TreePath)
 | 
						|
 | 
						|
			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("Render", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if ctx.Repo.GitRepo != nil {
 | 
						|
		checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
 | 
						|
		if checker != nil {
 | 
						|
			defer deferable()
 | 
						|
			attrs, err := checker.CheckPath(ctx.Repo.TreePath)
 | 
						|
			if err == nil {
 | 
						|
				ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
 | 
						|
				ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
 | 
						|
		img, _, err := image.DecodeConfig(bytes.NewReader(buf))
 | 
						|
		if err == nil {
 | 
						|
			// There are Image formats go can't decode
 | 
						|
			// Instead of throwing an error in that case, we show the size only when we can decode
 | 
						|
			ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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.RefFullName.IsBranch() {
 | 
						|
		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")
 | 
						|
	}
 | 
						|
}
 |