gitea源码

view_file.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "bytes"
  6. "fmt"
  7. "image"
  8. "io"
  9. "path"
  10. "strings"
  11. git_model "code.gitea.io/gitea/models/git"
  12. issue_model "code.gitea.io/gitea/models/issues"
  13. "code.gitea.io/gitea/models/renderhelper"
  14. user_model "code.gitea.io/gitea/models/user"
  15. "code.gitea.io/gitea/modules/actions"
  16. "code.gitea.io/gitea/modules/charset"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/git/attribute"
  19. "code.gitea.io/gitea/modules/highlight"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/markup"
  22. "code.gitea.io/gitea/modules/setting"
  23. "code.gitea.io/gitea/modules/typesniffer"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/services/context"
  26. issue_service "code.gitea.io/gitea/services/issue"
  27. "github.com/nektos/act/pkg/model"
  28. )
  29. func prepareLatestCommitInfo(ctx *context.Context) bool {
  30. commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
  31. if err != nil {
  32. ctx.ServerError("GetCommitByPath", err)
  33. return false
  34. }
  35. return loadLatestCommitData(ctx, commit)
  36. }
  37. func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) {
  38. attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
  39. Filenames: []string{ctx.Repo.TreePath},
  40. Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
  41. })
  42. if err != nil {
  43. ctx.ServerError("attribute.CheckAttributes", err)
  44. return nil, false
  45. }
  46. attrs := attrsMap[ctx.Repo.TreePath]
  47. if attrs == nil {
  48. // this case shouldn't happen, just in case.
  49. setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
  50. attrs = attribute.NewAttributes()
  51. }
  52. ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
  53. return attrs, true
  54. }
  55. func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
  56. markupType := markup.DetectMarkupTypeByFileName(filename)
  57. if markupType == "" {
  58. markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
  59. }
  60. if markupType == "" {
  61. return false
  62. }
  63. ctx.Data["HasSourceRenderedToggle"] = true
  64. if ctx.FormString("display") == "source" {
  65. return false
  66. }
  67. ctx.Data["MarkupType"] = markupType
  68. metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
  69. metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
  70. rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
  71. CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
  72. CurrentTreePath: path.Dir(ctx.Repo.TreePath),
  73. }).
  74. WithMarkupType(markupType).
  75. WithRelativePath(ctx.Repo.TreePath).
  76. WithMetas(metas)
  77. var err error
  78. ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
  79. if err != nil {
  80. ctx.ServerError("Render", err)
  81. return true
  82. }
  83. // to prevent iframe from loading third-party url
  84. ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
  85. return true
  86. }
  87. func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
  88. if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
  89. return false
  90. }
  91. if !fInfo.st.IsText() {
  92. if ctx.FormString("display") == "" {
  93. // not text but representable as text, e.g. SVG
  94. // since there is no "display" is specified, let other renders to handle
  95. return false
  96. }
  97. ctx.Data["HasSourceRenderedToggle"] = true
  98. }
  99. buf, _ := io.ReadAll(utf8Reader)
  100. // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
  101. // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
  102. // Gitea uses the definition (like most modern editors):
  103. // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
  104. // 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.
  105. // 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.
  106. // This NumLines is only used for the display on the UI: "xxx lines"
  107. if len(buf) == 0 {
  108. ctx.Data["NumLines"] = 0
  109. } else {
  110. ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
  111. }
  112. language := attrs.GetLanguage().Value()
  113. fileContent, lexerName, err := highlight.File(filename, language, buf)
  114. ctx.Data["LexerName"] = lexerName
  115. if err != nil {
  116. log.Error("highlight.File failed, fallback to plain text: %v", err)
  117. fileContent = highlight.PlainText(buf)
  118. }
  119. status := &charset.EscapeStatus{}
  120. statuses := make([]*charset.EscapeStatus, len(fileContent))
  121. for i, line := range fileContent {
  122. statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
  123. status = status.Or(statuses[i])
  124. }
  125. ctx.Data["EscapeStatus"] = status
  126. ctx.Data["FileContent"] = fileContent
  127. ctx.Data["LineEscapeStatus"] = statuses
  128. return true
  129. }
  130. func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool {
  131. if !fInfo.st.IsImage() {
  132. return false
  133. }
  134. if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled {
  135. return false
  136. }
  137. if fInfo.st.IsSvgImage() {
  138. ctx.Data["HasSourceRenderedToggle"] = true
  139. } else {
  140. img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf))
  141. if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
  142. ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
  143. }
  144. }
  145. return true
  146. }
  147. func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
  148. ctx.Data["IsViewFile"] = true
  149. ctx.Data["HideRepoInfo"] = true
  150. if !prepareLatestCommitInfo(ctx) {
  151. return
  152. }
  153. blob := entry.Blob()
  154. ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
  155. ctx.Data["FileIsSymlink"] = entry.IsLink()
  156. ctx.Data["FileTreePath"] = ctx.Repo.TreePath
  157. ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  158. if ctx.Repo.TreePath == ".editorconfig" {
  159. _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
  160. if editorconfigWarning != nil {
  161. ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
  162. }
  163. if editorconfigErr != nil {
  164. ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
  165. }
  166. } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
  167. _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
  168. if issueConfigErr != nil {
  169. ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
  170. }
  171. } else if actions.IsWorkflow(ctx.Repo.TreePath) {
  172. content, err := actions.GetContentFromEntry(entry)
  173. if err != nil {
  174. log.Error("actions.GetContentFromEntry: %v", err)
  175. }
  176. _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
  177. if workFlowErr != nil {
  178. ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
  179. }
  180. } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
  181. if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
  182. _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
  183. if len(warnings) > 0 {
  184. ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
  185. }
  186. }
  187. }
  188. // Don't call any other repository functions depends on git.Repository until the dataRc closed to
  189. // avoid creating an unnecessary temporary cat file.
  190. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
  191. if err != nil {
  192. ctx.ServerError("getFileReader", err)
  193. return
  194. }
  195. defer dataRc.Close()
  196. if fInfo.isLFSFile() {
  197. ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  198. }
  199. if !prepareFileViewEditorButtons(ctx) {
  200. return
  201. }
  202. ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
  203. ctx.Data["FileSize"] = fInfo.fileSize
  204. ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText()
  205. ctx.Data["IsExecutable"] = entry.IsExecutable()
  206. ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage()
  207. attrs, ok := prepareFileViewLfsAttrs(ctx)
  208. if !ok {
  209. return
  210. }
  211. // TODO: in the future maybe we need more accurate flags, for example:
  212. // * IsRepresentableAsText: some files are text, some are not
  213. // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
  214. // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
  215. utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
  216. switch {
  217. case fInfo.fileSize >= setting.UI.MaxDisplayFileSize:
  218. ctx.Data["IsFileTooLarge"] = true
  219. case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
  220. // it also sets ctx.Data["FileContent"] and more
  221. ctx.Data["IsMarkup"] = true
  222. case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
  223. // it also sets ctx.Data["FileContent"] and more
  224. ctx.Data["IsDisplayingSource"] = true
  225. case handleFileViewRenderImage(ctx, fInfo, buf):
  226. ctx.Data["IsImageFile"] = true
  227. case fInfo.st.IsVideo():
  228. ctx.Data["IsVideoFile"] = true
  229. case fInfo.st.IsAudio():
  230. ctx.Data["IsAudioFile"] = true
  231. default:
  232. // unable to render anything, show the "view raw" or let frontend handle it
  233. }
  234. }
  235. func prepareFileViewEditorButtons(ctx *context.Context) bool {
  236. // archived or mirror repository, the buttons should not be shown
  237. if !ctx.Repo.Repository.CanEnableEditor() {
  238. return true
  239. }
  240. // The buttons should not be shown if it's not a branch
  241. if !ctx.Repo.RefFullName.IsBranch() {
  242. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
  243. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
  244. return true
  245. }
  246. if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
  247. ctx.Data["CanEditFile"] = true
  248. ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
  249. ctx.Data["CanDeleteFile"] = true
  250. ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
  251. return true
  252. }
  253. lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
  254. ctx.Data["LFSLock"] = lfsLock
  255. if err != nil {
  256. ctx.ServerError("GetTreePathLock", err)
  257. return false
  258. }
  259. if lfsLock != nil {
  260. u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
  261. if err != nil {
  262. ctx.ServerError("GetTreePathLock", err)
  263. return false
  264. }
  265. ctx.Data["LFSLockOwner"] = u.Name
  266. ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
  267. ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
  268. }
  269. // it's a lfs file and the user is not the owner of the lock
  270. isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID
  271. ctx.Data["CanEditFile"] = !isLFSLocked
  272. ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file"))
  273. ctx.Data["CanDeleteFile"] = !isLFSLocked
  274. ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file"))
  275. return true
  276. }