code.gitea.io/gitea@v1.21.7/routers/web/repo/view.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // Copyright 2014 The Gogs Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 "bytes" 9 gocontext "context" 10 "encoding/base64" 11 "fmt" 12 "html/template" 13 "image" 14 "io" 15 "net/http" 16 "net/url" 17 "path" 18 "slices" 19 "strings" 20 "time" 21 22 _ "image/gif" // for processing gif images 23 _ "image/jpeg" // for processing jpeg images 24 _ "image/png" // for processing png images 25 26 activities_model "code.gitea.io/gitea/models/activities" 27 admin_model "code.gitea.io/gitea/models/admin" 28 asymkey_model "code.gitea.io/gitea/models/asymkey" 29 "code.gitea.io/gitea/models/db" 30 git_model "code.gitea.io/gitea/models/git" 31 issue_model "code.gitea.io/gitea/models/issues" 32 repo_model "code.gitea.io/gitea/models/repo" 33 unit_model "code.gitea.io/gitea/models/unit" 34 user_model "code.gitea.io/gitea/models/user" 35 "code.gitea.io/gitea/modules/actions" 36 "code.gitea.io/gitea/modules/base" 37 "code.gitea.io/gitea/modules/charset" 38 "code.gitea.io/gitea/modules/container" 39 "code.gitea.io/gitea/modules/context" 40 "code.gitea.io/gitea/modules/git" 41 "code.gitea.io/gitea/modules/highlight" 42 "code.gitea.io/gitea/modules/lfs" 43 "code.gitea.io/gitea/modules/log" 44 "code.gitea.io/gitea/modules/markup" 45 repo_module "code.gitea.io/gitea/modules/repository" 46 "code.gitea.io/gitea/modules/setting" 47 "code.gitea.io/gitea/modules/structs" 48 "code.gitea.io/gitea/modules/typesniffer" 49 "code.gitea.io/gitea/modules/util" 50 "code.gitea.io/gitea/routers/web/feed" 51 issue_service "code.gitea.io/gitea/services/issue" 52 53 "github.com/nektos/act/pkg/model" 54 55 _ "golang.org/x/image/bmp" // for processing bmp images 56 _ "golang.org/x/image/webp" // for processing webp images 57 ) 58 59 const ( 60 tplRepoEMPTY base.TplName = "repo/empty" 61 tplRepoHome base.TplName = "repo/home" 62 tplRepoViewList base.TplName = "repo/view_list" 63 tplWatchers base.TplName = "repo/watchers" 64 tplForks base.TplName = "repo/forks" 65 tplMigrating base.TplName = "repo/migrate/migrating" 66 ) 67 68 // locate a README for a tree in one of the supported paths. 69 // 70 // entries is passed to reduce calls to ListEntries(), so 71 // this has precondition: 72 // 73 // entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() 74 // 75 // FIXME: There has to be a more efficient way of doing this 76 func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { 77 // Create a list of extensions in priority order 78 // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md 79 // 2. Txt files - e.g. README.txt 80 // 3. No extension - e.g. README 81 exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority 82 extCount := len(exts) 83 readmeFiles := make([]*git.TreeEntry, extCount+1) 84 85 docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) 86 for _, entry := range entries { 87 if tryWellKnownDirs && entry.IsDir() { 88 // as a special case for the top-level repo introduction README, 89 // fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ... 90 // (note that docsEntries is ignored unless we are at the root) 91 lowerName := strings.ToLower(entry.Name()) 92 switch lowerName { 93 case "docs": 94 if entry.Name() == "docs" || docsEntries[0] == nil { 95 docsEntries[0] = entry 96 } 97 case ".gitea": 98 if entry.Name() == ".gitea" || docsEntries[1] == nil { 99 docsEntries[1] = entry 100 } 101 case ".github": 102 if entry.Name() == ".github" || docsEntries[2] == nil { 103 docsEntries[2] = entry 104 } 105 } 106 continue 107 } 108 if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { 109 log.Debug("Potential readme file: %s", entry.Name()) 110 if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { 111 if entry.IsLink() { 112 target, err := entry.FollowLinks() 113 if err != nil && !git.IsErrBadLink(err) { 114 return "", nil, err 115 } else if target != nil && (target.IsExecutable() || target.IsRegular()) { 116 readmeFiles[i] = entry 117 } 118 } else { 119 readmeFiles[i] = entry 120 } 121 } 122 } 123 } 124 var readmeFile *git.TreeEntry 125 for _, f := range readmeFiles { 126 if f != nil { 127 readmeFile = f 128 break 129 } 130 } 131 132 if ctx.Repo.TreePath == "" && readmeFile == nil { 133 for _, subTreeEntry := range docsEntries { 134 if subTreeEntry == nil { 135 continue 136 } 137 subTree := subTreeEntry.Tree() 138 if subTree == nil { 139 // this should be impossible; if subTreeEntry exists so should this. 140 continue 141 } 142 var err error 143 childEntries, err := subTree.ListEntries() 144 if err != nil { 145 return "", nil, err 146 } 147 148 subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) 149 if err != nil && !git.IsErrNotExist(err) { 150 return "", nil, err 151 } 152 if readmeFile != nil { 153 return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil 154 } 155 } 156 } 157 158 return "", readmeFile, nil 159 } 160 161 func renderDirectory(ctx *context.Context) { 162 entries := renderDirectoryFiles(ctx, 1*time.Second) 163 if ctx.Written() { 164 return 165 } 166 167 if ctx.Repo.TreePath != "" { 168 ctx.Data["HideRepoInfo"] = true 169 ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) 170 } 171 172 subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) 173 if err != nil { 174 ctx.ServerError("findReadmeFileInEntries", err) 175 return 176 } 177 178 renderReadmeFile(ctx, subfolder, readmeFile) 179 } 180 181 // localizedExtensions prepends the provided language code with and without a 182 // regional identifier to the provided extension. 183 // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` 184 // Note: ext should be prefixed with a `.` 185 func localizedExtensions(ext, languageCode string) (localizedExts []string) { 186 if len(languageCode) < 1 { 187 return []string{ext} 188 } 189 190 lowerLangCode := "." + strings.ToLower(languageCode) 191 192 if strings.Contains(lowerLangCode, "-") { 193 underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") 194 indexOfDash := strings.Index(lowerLangCode, "-") 195 // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] 196 return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} 197 } 198 199 // e.g. [.en.md, .md] 200 return []string{lowerLangCode + ext, ext} 201 } 202 203 type fileInfo struct { 204 isTextFile bool 205 isLFSFile bool 206 fileSize int64 207 lfsMeta *lfs.Pointer 208 st typesniffer.SniffedType 209 } 210 211 func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) { 212 dataRc, err := blob.DataAsync() 213 if err != nil { 214 return nil, nil, nil, err 215 } 216 217 buf := make([]byte, 1024) 218 n, _ := util.ReadAtMost(dataRc, buf) 219 buf = buf[:n] 220 221 st := typesniffer.DetectContentType(buf) 222 isTextFile := st.IsText() 223 224 // FIXME: what happens when README file is an image? 225 if !isTextFile || !setting.LFS.StartServer { 226 return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil 227 } 228 229 pointer, _ := lfs.ReadPointerFromBuffer(buf) 230 if !pointer.IsValid() { // fallback to plain file 231 return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil 232 } 233 234 meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid) 235 if err != nil && err != git_model.ErrLFSObjectNotExist { // fallback to plain file 236 return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil 237 } 238 239 dataRc.Close() 240 if err != nil { 241 return nil, nil, nil, err 242 } 243 244 dataRc, err = lfs.ReadMetaObject(pointer) 245 if err != nil { 246 return nil, nil, nil, err 247 } 248 249 buf = make([]byte, 1024) 250 n, err = util.ReadAtMost(dataRc, buf) 251 if err != nil { 252 dataRc.Close() 253 return nil, nil, nil, err 254 } 255 buf = buf[:n] 256 257 st = typesniffer.DetectContentType(buf) 258 259 return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil 260 } 261 262 func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { 263 target := readmeFile 264 if readmeFile != nil && readmeFile.IsLink() { 265 target, _ = readmeFile.FollowLinks() 266 } 267 if target == nil { 268 // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) 269 // simply skip rendering the README 270 return 271 } 272 273 ctx.Data["RawFileLink"] = "" 274 ctx.Data["ReadmeInList"] = true 275 ctx.Data["ReadmeExist"] = true 276 ctx.Data["FileIsSymlink"] = readmeFile.IsLink() 277 278 buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) 279 if err != nil { 280 ctx.ServerError("getFileReader", err) 281 return 282 } 283 defer dataRc.Close() 284 285 ctx.Data["FileIsText"] = fInfo.isTextFile 286 ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name()) 287 ctx.Data["IsLFSFile"] = fInfo.isLFSFile 288 289 if fInfo.isLFSFile { 290 filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) 291 ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) 292 } 293 294 if !fInfo.isTextFile { 295 return 296 } 297 298 if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { 299 // Pretend that this is a normal text file to display 'This file is too large to be shown' 300 ctx.Data["IsFileTooLarge"] = true 301 ctx.Data["IsTextFile"] = true 302 ctx.Data["FileSize"] = fInfo.fileSize 303 return 304 } 305 306 rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) 307 308 if markupType := markup.Type(readmeFile.Name()); markupType != "" { 309 ctx.Data["IsMarkup"] = true 310 ctx.Data["MarkupType"] = markupType 311 312 ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ 313 Ctx: ctx, 314 RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). 315 Links: markup.Links{ 316 Base: ctx.Repo.RepoLink, 317 BranchPath: ctx.Repo.BranchNameSubURL(), 318 TreePath: path.Join(ctx.Repo.TreePath, subfolder), 319 }, 320 Metas: ctx.Repo.Repository.ComposeDocumentMetas(), 321 GitRepo: ctx.Repo.GitRepo, 322 }, rd) 323 if err != nil { 324 log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) 325 delete(ctx.Data, "IsMarkup") 326 } 327 } 328 329 if ctx.Data["IsMarkup"] != true { 330 ctx.Data["IsPlainText"] = true 331 content, err := io.ReadAll(rd) 332 if err != nil { 333 log.Error("Read readme content failed: %v", err) 334 } 335 contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) 336 ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) 337 } 338 } 339 340 func renderFile(ctx *context.Context, entry *git.TreeEntry) { 341 ctx.Data["IsViewFile"] = true 342 ctx.Data["HideRepoInfo"] = true 343 blob := entry.Blob() 344 buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) 345 if err != nil { 346 ctx.ServerError("getFileReader", err) 347 return 348 } 349 defer dataRc.Close() 350 351 ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) 352 ctx.Data["FileIsSymlink"] = entry.IsLink() 353 ctx.Data["FileName"] = blob.Name() 354 ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) 355 356 if ctx.Repo.TreePath == ".editorconfig" { 357 _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) 358 if editorconfigWarning != nil { 359 ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) 360 } 361 if editorconfigErr != nil { 362 ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) 363 } 364 } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { 365 _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) 366 if issueConfigErr != nil { 367 ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) 368 } 369 } else if actions.IsWorkflow(ctx.Repo.TreePath) { 370 content, err := actions.GetContentFromEntry(entry) 371 if err != nil { 372 log.Error("actions.GetContentFromEntry: %v", err) 373 } 374 _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) 375 if workFlowErr != nil { 376 ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) 377 } 378 } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { 379 if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { 380 _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) 381 if len(warnings) > 0 { 382 ctx.Data["FileWarning"] = strings.Join(warnings, "\n") 383 } 384 } 385 } 386 387 isDisplayingSource := ctx.FormString("display") == "source" 388 isDisplayingRendered := !isDisplayingSource 389 390 if fInfo.isLFSFile { 391 ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) 392 } 393 394 isRepresentableAsText := fInfo.st.IsRepresentableAsText() 395 if !isRepresentableAsText { 396 // If we can't show plain text, always try to render. 397 isDisplayingSource = false 398 isDisplayingRendered = true 399 } 400 ctx.Data["IsLFSFile"] = fInfo.isLFSFile 401 ctx.Data["FileSize"] = fInfo.fileSize 402 ctx.Data["IsTextFile"] = fInfo.isTextFile 403 ctx.Data["IsRepresentableAsText"] = isRepresentableAsText 404 ctx.Data["IsDisplayingSource"] = isDisplayingSource 405 ctx.Data["IsDisplayingRendered"] = isDisplayingRendered 406 ctx.Data["IsExecutable"] = entry.IsExecutable() 407 408 isTextSource := fInfo.isTextFile || isDisplayingSource 409 ctx.Data["IsTextSource"] = isTextSource 410 if isTextSource { 411 ctx.Data["CanCopyContent"] = true 412 } 413 414 // Check LFS Lock 415 lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) 416 ctx.Data["LFSLock"] = lfsLock 417 if err != nil { 418 ctx.ServerError("GetTreePathLock", err) 419 return 420 } 421 if lfsLock != nil { 422 u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) 423 if err != nil { 424 ctx.ServerError("GetTreePathLock", err) 425 return 426 } 427 ctx.Data["LFSLockOwner"] = u.Name 428 ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() 429 ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") 430 } 431 432 // Assume file is not editable first. 433 if fInfo.isLFSFile { 434 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") 435 } else if !isRepresentableAsText { 436 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") 437 } 438 439 switch { 440 case isRepresentableAsText: 441 if fInfo.st.IsSvgImage() { 442 ctx.Data["IsImageFile"] = true 443 ctx.Data["CanCopyContent"] = true 444 ctx.Data["HasSourceRenderedToggle"] = true 445 } 446 447 if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { 448 ctx.Data["IsFileTooLarge"] = true 449 break 450 } 451 452 rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) 453 454 shouldRenderSource := ctx.FormString("display") == "source" 455 readmeExist := util.IsReadmeFileName(blob.Name()) 456 ctx.Data["ReadmeExist"] = readmeExist 457 458 markupType := markup.Type(blob.Name()) 459 // If the markup is detected by custom markup renderer it should not be reset later on 460 // to not pass it down to the render context. 461 detected := false 462 if markupType == "" { 463 detected = true 464 markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) 465 } 466 if markupType != "" { 467 ctx.Data["HasSourceRenderedToggle"] = true 468 } 469 470 if markupType != "" && !shouldRenderSource { 471 ctx.Data["IsMarkup"] = true 472 ctx.Data["MarkupType"] = markupType 473 if !detected { 474 markupType = "" 475 } 476 metas := ctx.Repo.Repository.ComposeDocumentMetas() 477 metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() 478 ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ 479 Ctx: ctx, 480 Type: markupType, 481 RelativePath: ctx.Repo.TreePath, 482 Links: markup.Links{ 483 Base: ctx.Repo.RepoLink, 484 BranchPath: ctx.Repo.BranchNameSubURL(), 485 TreePath: path.Dir(ctx.Repo.TreePath), 486 }, 487 Metas: metas, 488 GitRepo: ctx.Repo.GitRepo, 489 }, rd) 490 if err != nil { 491 ctx.ServerError("Render", err) 492 return 493 } 494 // to prevent iframe load third-party url 495 ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") 496 } else { 497 buf, _ := io.ReadAll(rd) 498 499 // empty: 0 lines; "a": one line; "a\n": two lines; "a\nb": two lines; 500 // the NumLines is only used for the display on the UI: "xxx lines" 501 if len(buf) == 0 { 502 ctx.Data["NumLines"] = 0 503 } else { 504 ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 505 } 506 ctx.Data["NumLinesSet"] = true 507 508 language := "" 509 510 indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID) 511 if err == nil { 512 defer deleteTemporaryFile() 513 514 filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{ 515 CachedOnly: true, 516 Attributes: []string{"linguist-language", "gitlab-language"}, 517 Filenames: []string{ctx.Repo.TreePath}, 518 IndexFile: indexFilename, 519 WorkTree: worktree, 520 }) 521 if err != nil { 522 log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) 523 } 524 525 language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"] 526 if language == "" || language == "unspecified" { 527 language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"] 528 } 529 if language == "unspecified" { 530 language = "" 531 } 532 } 533 fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) 534 ctx.Data["LexerName"] = lexerName 535 if err != nil { 536 log.Error("highlight.File failed, fallback to plain text: %v", err) 537 fileContent = highlight.PlainText(buf) 538 } 539 status := &charset.EscapeStatus{} 540 statuses := make([]*charset.EscapeStatus, len(fileContent)) 541 for i, line := range fileContent { 542 statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) 543 status = status.Or(statuses[i]) 544 } 545 ctx.Data["EscapeStatus"] = status 546 ctx.Data["FileContent"] = fileContent 547 ctx.Data["LineEscapeStatus"] = statuses 548 } 549 if !fInfo.isLFSFile { 550 if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { 551 if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { 552 ctx.Data["CanEditFile"] = false 553 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") 554 } else { 555 ctx.Data["CanEditFile"] = true 556 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") 557 } 558 } else if !ctx.Repo.IsViewBranch { 559 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") 560 } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { 561 ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") 562 } 563 } 564 565 case fInfo.st.IsPDF(): 566 ctx.Data["IsPDFFile"] = true 567 case fInfo.st.IsVideo(): 568 ctx.Data["IsVideoFile"] = true 569 case fInfo.st.IsAudio(): 570 ctx.Data["IsAudioFile"] = true 571 case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): 572 ctx.Data["IsImageFile"] = true 573 ctx.Data["CanCopyContent"] = true 574 default: 575 if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { 576 ctx.Data["IsFileTooLarge"] = true 577 break 578 } 579 580 if markupType := markup.Type(blob.Name()); markupType != "" { 581 rd := io.MultiReader(bytes.NewReader(buf), dataRc) 582 ctx.Data["IsMarkup"] = true 583 ctx.Data["MarkupType"] = markupType 584 ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ 585 Ctx: ctx, 586 RelativePath: ctx.Repo.TreePath, 587 Links: markup.Links{ 588 Base: ctx.Repo.RepoLink, 589 BranchPath: ctx.Repo.BranchNameSubURL(), 590 TreePath: path.Dir(ctx.Repo.TreePath), 591 }, 592 Metas: ctx.Repo.Repository.ComposeDocumentMetas(), 593 GitRepo: ctx.Repo.GitRepo, 594 }, rd) 595 if err != nil { 596 ctx.ServerError("Render", err) 597 return 598 } 599 } 600 } 601 602 if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { 603 img, _, err := image.DecodeConfig(bytes.NewReader(buf)) 604 if err == nil { 605 // There are Image formats go can't decode 606 // Instead of throwing an error in that case, we show the size only when we can decode 607 ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) 608 } 609 } 610 611 if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { 612 if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { 613 ctx.Data["CanDeleteFile"] = false 614 ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") 615 } else { 616 ctx.Data["CanDeleteFile"] = true 617 ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") 618 } 619 } else if !ctx.Repo.IsViewBranch { 620 ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") 621 } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { 622 ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") 623 } 624 } 625 626 func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { 627 markupRd, markupWr := io.Pipe() 628 defer markupWr.Close() 629 done := make(chan struct{}) 630 go func() { 631 sb := &strings.Builder{} 632 // We allow NBSP here this is rendered 633 escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) 634 output = template.HTML(sb.String()) 635 close(done) 636 }() 637 err = markup.Render(renderCtx, input, markupWr) 638 _ = markupWr.CloseWithError(err) 639 <-done 640 return escaped, output, err 641 } 642 643 func checkHomeCodeViewable(ctx *context.Context) { 644 if len(ctx.Repo.Units) > 0 { 645 if ctx.Repo.Repository.IsBeingCreated() { 646 task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID) 647 if err != nil { 648 if admin_model.IsErrTaskDoesNotExist(err) { 649 ctx.Data["Repo"] = ctx.Repo 650 ctx.Data["CloneAddr"] = "" 651 ctx.Data["Failed"] = true 652 ctx.HTML(http.StatusOK, tplMigrating) 653 return 654 } 655 ctx.ServerError("models.GetMigratingTask", err) 656 return 657 } 658 cfg, err := task.MigrateConfig() 659 if err != nil { 660 ctx.ServerError("task.MigrateConfig", err) 661 return 662 } 663 664 ctx.Data["Repo"] = ctx.Repo 665 ctx.Data["MigrateTask"] = task 666 ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr) 667 ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed 668 ctx.HTML(http.StatusOK, tplMigrating) 669 return 670 } 671 672 if ctx.IsSigned { 673 // Set repo notification-status read if unread 674 if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil { 675 ctx.ServerError("ReadBy", err) 676 return 677 } 678 } 679 680 var firstUnit *unit_model.Unit 681 for _, repoUnit := range ctx.Repo.Units { 682 if repoUnit.Type == unit_model.TypeCode { 683 return 684 } 685 686 unit, ok := unit_model.Units[repoUnit.Type] 687 if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) { 688 firstUnit = &unit 689 } 690 } 691 692 if firstUnit != nil { 693 ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI)) 694 return 695 } 696 } 697 698 ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo"))) 699 } 700 701 func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) { 702 if entry.Name() != "" { 703 return 704 } 705 tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) 706 if err != nil { 707 HandleGitError(ctx, "Repo.Commit.SubTree", err) 708 return 709 } 710 allEntries, err := tree.ListEntries() 711 if err != nil { 712 ctx.ServerError("ListEntries", err) 713 return 714 } 715 for _, entry := range allEntries { 716 if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { 717 // Read Citation file contents 718 if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { 719 log.Error("checkCitationFile: GetBlobContent: %v", err) 720 } else { 721 ctx.Data["CitiationExist"] = true 722 ctx.PageData["citationFileContent"] = content 723 break 724 } 725 } 726 } 727 } 728 729 // Home render repository home page 730 func Home(ctx *context.Context) { 731 if setting.Other.EnableFeed { 732 isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req) 733 if isFeed { 734 switch { 735 case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): 736 feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) 737 case ctx.Repo.TreePath == "": 738 feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) 739 case ctx.Repo.TreePath != "": 740 feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) 741 } 742 return 743 } 744 } 745 746 checkHomeCodeViewable(ctx) 747 if ctx.Written() { 748 return 749 } 750 751 renderCode(ctx) 752 } 753 754 // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body 755 func LastCommit(ctx *context.Context) { 756 checkHomeCodeViewable(ctx) 757 if ctx.Written() { 758 return 759 } 760 761 renderDirectoryFiles(ctx, 0) 762 if ctx.Written() { 763 return 764 } 765 766 var treeNames []string 767 paths := make([]string, 0, 5) 768 if len(ctx.Repo.TreePath) > 0 { 769 treeNames = strings.Split(ctx.Repo.TreePath, "/") 770 for i := range treeNames { 771 paths = append(paths, strings.Join(treeNames[:i+1], "/")) 772 } 773 774 ctx.Data["HasParentPath"] = true 775 if len(paths)-2 >= 0 { 776 ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] 777 } 778 } 779 branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() 780 ctx.Data["BranchLink"] = branchLink 781 782 ctx.HTML(http.StatusOK, tplRepoViewList) 783 } 784 785 func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries { 786 tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) 787 if err != nil { 788 HandleGitError(ctx, "Repo.Commit.SubTree", err) 789 return nil 790 } 791 792 ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) 793 794 // Get current entry user currently looking at. 795 entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) 796 if err != nil { 797 HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) 798 return nil 799 } 800 801 if !entry.IsDir() { 802 HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) 803 return nil 804 } 805 806 allEntries, err := tree.ListEntries() 807 if err != nil { 808 ctx.ServerError("ListEntries", err) 809 return nil 810 } 811 allEntries.CustomSort(base.NaturalSortLess) 812 813 commitInfoCtx := gocontext.Context(ctx) 814 if timeout > 0 { 815 var cancel gocontext.CancelFunc 816 commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout) 817 defer cancel() 818 } 819 820 selected := make(container.Set[string]) 821 selected.AddMultiple(ctx.FormStrings("f[]")...) 822 823 entries := allEntries 824 if len(selected) > 0 { 825 entries = make(git.Entries, 0, len(selected)) 826 for _, entry := range allEntries { 827 if selected.Contains(entry.Name()) { 828 entries = append(entries, entry) 829 } 830 } 831 } 832 833 var latestCommit *git.Commit 834 ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) 835 if err != nil { 836 ctx.ServerError("GetCommitsInfo", err) 837 return nil 838 } 839 840 // Show latest commit info of repository in table header, 841 // or of directory if not in root directory. 842 ctx.Data["LatestCommit"] = latestCommit 843 if latestCommit != nil { 844 845 verification := asymkey_model.ParseCommitWithSignature(ctx, latestCommit) 846 847 if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) { 848 return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID) 849 }, nil); err != nil { 850 ctx.ServerError("CalculateTrustStatus", err) 851 return nil 852 } 853 ctx.Data["LatestCommitVerification"] = verification 854 ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit) 855 856 statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true}) 857 if err != nil { 858 log.Error("GetLatestCommitStatus: %v", err) 859 } 860 861 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses) 862 ctx.Data["LatestCommitStatuses"] = statuses 863 } 864 865 branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() 866 treeLink := branchLink 867 868 if len(ctx.Repo.TreePath) > 0 { 869 treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) 870 } 871 872 ctx.Data["TreeLink"] = treeLink 873 ctx.Data["SSHDomain"] = setting.SSH.Domain 874 875 return allEntries 876 } 877 878 func renderLanguageStats(ctx *context.Context) { 879 langs, err := repo_model.GetTopLanguageStats(ctx.Repo.Repository, 5) 880 if err != nil { 881 ctx.ServerError("Repo.GetTopLanguageStats", err) 882 return 883 } 884 885 ctx.Data["LanguageStats"] = langs 886 } 887 888 func renderRepoTopics(ctx *context.Context) { 889 topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{ 890 RepoID: ctx.Repo.Repository.ID, 891 }) 892 if err != nil { 893 ctx.ServerError("models.FindTopics", err) 894 return 895 } 896 ctx.Data["Topics"] = topics 897 } 898 899 func renderCode(ctx *context.Context) { 900 ctx.Data["PageIsViewCode"] = true 901 ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled 902 903 if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { 904 showEmpty := true 905 var err error 906 if ctx.Repo.GitRepo != nil { 907 showEmpty, err = ctx.Repo.GitRepo.IsEmpty() 908 if err != nil { 909 log.Error("GitRepo.IsEmpty: %v", err) 910 ctx.Repo.Repository.Status = repo_model.RepositoryBroken 911 showEmpty = true 912 ctx.Flash.Error(ctx.Tr("error.occurred"), true) 913 } 914 } 915 if showEmpty { 916 ctx.HTML(http.StatusOK, tplRepoEMPTY) 917 return 918 } 919 920 // the repo is not really empty, so we should update the modal in database 921 // such problem may be caused by: 922 // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually 923 // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. 924 // it's possible for a repository to be non-empty by that flag but still 500 925 // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. 926 ctx.Repo.Repository.IsEmpty = false 927 if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { 928 ctx.ServerError("UpdateRepositoryCols", err) 929 return 930 } 931 if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { 932 ctx.ServerError("UpdateRepoSize", err) 933 return 934 } 935 936 // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values 937 link := ctx.Link 938 if ctx.Req.URL.RawQuery != "" { 939 link += "?" + ctx.Req.URL.RawQuery 940 } 941 ctx.Redirect(link) 942 return 943 } 944 945 title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name 946 if len(ctx.Repo.Repository.Description) > 0 { 947 title += ": " + ctx.Repo.Repository.Description 948 } 949 ctx.Data["Title"] = title 950 951 // Get Topics of this repo 952 renderRepoTopics(ctx) 953 if ctx.Written() { 954 return 955 } 956 957 // Get current entry user currently looking at. 958 entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) 959 if err != nil { 960 HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) 961 return 962 } 963 964 checkCitationFile(ctx, entry) 965 if ctx.Written() { 966 return 967 } 968 969 renderLanguageStats(ctx) 970 if ctx.Written() { 971 return 972 } 973 974 if entry.IsDir() { 975 renderDirectory(ctx) 976 } else { 977 renderFile(ctx, entry) 978 } 979 if ctx.Written() { 980 return 981 } 982 983 if ctx.Doer != nil { 984 if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { 985 ctx.ServerError("GetBaseRepo", err) 986 return 987 } 988 989 showRecentlyPushedNewBranches := true 990 if ctx.Repo.Repository.IsMirror || 991 !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) { 992 showRecentlyPushedNewBranches = false 993 } 994 if showRecentlyPushedNewBranches { 995 ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch) 996 if err != nil { 997 ctx.ServerError("GetRecentlyPushedBranches", err) 998 return 999 } 1000 } 1001 } 1002 1003 var treeNames []string 1004 paths := make([]string, 0, 5) 1005 if len(ctx.Repo.TreePath) > 0 { 1006 treeNames = strings.Split(ctx.Repo.TreePath, "/") 1007 for i := range treeNames { 1008 paths = append(paths, strings.Join(treeNames[:i+1], "/")) 1009 } 1010 1011 ctx.Data["HasParentPath"] = true 1012 if len(paths)-2 >= 0 { 1013 ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] 1014 } 1015 } 1016 1017 ctx.Data["Paths"] = paths 1018 1019 branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() 1020 treeLink := branchLink 1021 if len(ctx.Repo.TreePath) > 0 { 1022 treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) 1023 } 1024 ctx.Data["TreeLink"] = treeLink 1025 ctx.Data["TreeNames"] = treeNames 1026 ctx.Data["BranchLink"] = branchLink 1027 ctx.HTML(http.StatusOK, tplRepoHome) 1028 } 1029 1030 // RenderUserCards render a page show users according the input template 1031 func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { 1032 page := ctx.FormInt("page") 1033 if page <= 0 { 1034 page = 1 1035 } 1036 pager := context.NewPagination(total, setting.ItemsPerPage, page, 5) 1037 ctx.Data["Page"] = pager 1038 1039 items, err := getter(db.ListOptions{ 1040 Page: pager.Paginater.Current(), 1041 PageSize: setting.ItemsPerPage, 1042 }) 1043 if err != nil { 1044 ctx.ServerError("getter", err) 1045 return 1046 } 1047 ctx.Data["Cards"] = items 1048 1049 ctx.HTML(http.StatusOK, tpl) 1050 } 1051 1052 // Watchers render repository's watch users 1053 func Watchers(ctx *context.Context) { 1054 ctx.Data["Title"] = ctx.Tr("repo.watchers") 1055 ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers") 1056 ctx.Data["PageIsWatchers"] = true 1057 1058 RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) { 1059 return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts) 1060 }, tplWatchers) 1061 } 1062 1063 // Stars render repository's starred users 1064 func Stars(ctx *context.Context) { 1065 ctx.Data["Title"] = ctx.Tr("repo.stargazers") 1066 ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers") 1067 ctx.Data["PageIsStargazers"] = true 1068 RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) { 1069 return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts) 1070 }, tplWatchers) 1071 } 1072 1073 // Forks render repository's forked users 1074 func Forks(ctx *context.Context) { 1075 ctx.Data["Title"] = ctx.Tr("repo.forks") 1076 1077 page := ctx.FormInt("page") 1078 if page <= 0 { 1079 page = 1 1080 } 1081 1082 pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5) 1083 ctx.Data["Page"] = pager 1084 1085 forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{ 1086 Page: pager.Paginater.Current(), 1087 PageSize: setting.ItemsPerPage, 1088 }) 1089 if err != nil { 1090 ctx.ServerError("GetForks", err) 1091 return 1092 } 1093 1094 for _, fork := range forks { 1095 if err = fork.LoadOwner(ctx); err != nil { 1096 ctx.ServerError("LoadOwner", err) 1097 return 1098 } 1099 } 1100 1101 ctx.Data["Forks"] = forks 1102 1103 ctx.HTML(http.StatusOK, tplForks) 1104 }