From bbffcc3aecda040a40d49078e7141213bc8d78af Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 16 Nov 2021 18:18:25 +0000 Subject: [PATCH] Multiple Escaping Improvements (#17551) There are multiple places where Gitea does not properly escape URLs that it is building and there are multiple places where it builds urls when there is already a simpler function available to use this. This is an extensive PR attempting to fix these issues. 1. The first commit in this PR looks through all href, src and links in the Gitea codebase and has attempted to catch all the places where there is potentially incomplete escaping. 2. Whilst doing this we will prefer to use functions that create URLs over recreating them by hand. 3. All uses of strings should be directly escaped - even if they are not currently expected to contain escaping characters. The main benefit to doing this will be that we can consider relaxing the constraints on user names and reponames in future. 4. The next commit looks at escaping in the wiki and re-considers the urls that are used there. Using the improved escaping here wiki files containing '/'. (This implementation will currently still place all of the wiki files the root directory of the repo but this would not be difficult to change.) 5. The title generation in feeds is now properly escaped. 6. EscapePound is no longer needed - urls should be PathEscaped / QueryEscaped as necessary but then re-escaped with Escape when creating html with locales Signed-off-by: Andrew Thornton Signed-off-by: Andrew Thornton --- integrations/issue_test.go | 4 +- integrations/links_test.go | 2 +- integrations/nonascii_branches_test.go | 32 ++--- models/action.go | 7 +- models/attachment.go | 3 +- models/avatars/avatar.go | 8 +- models/commit_status.go | 4 +- models/issue.go | 11 ++ models/notification.go | 3 +- models/release.go | 11 +- models/repo.go | 21 ++- models/repo_avatar.go | 3 +- models/user.go | 7 +- modules/context/context.go | 16 ++- modules/context/repo.go | 38 ++--- modules/convert/git_commit.go | 11 +- modules/convert/issue.go | 3 +- modules/convert/notification.go | 4 +- modules/git/utils.go | 4 +- modules/repofiles/action.go | 3 +- modules/repofiles/blob.go | 4 +- modules/repofiles/file.go | 8 +- modules/repofiles/tree.go | 3 +- modules/repository/commits.go | 3 +- modules/templates/helper.go | 21 ++- modules/upload/upload.go | 3 +- options/locale/locale_en-US.ini | 38 ++--- routers/api/v1/org/member.go | 3 +- routers/api/v1/repo/git_ref.go | 6 +- routers/api/v1/repo/key.go | 11 +- routers/web/admin/auths.go | 6 +- routers/web/admin/repos.go | 4 +- routers/web/admin/users.go | 10 +- routers/web/feed/convert.go | 134 ++++++++++++++---- routers/web/org/setting.go | 3 +- routers/web/org/teams.go | 19 +-- routers/web/repo/blame.go | 10 +- routers/web/repo/commit.go | 4 +- routers/web/repo/compare.go | 33 +++-- routers/web/repo/editor.go | 12 +- routers/web/repo/issue.go | 25 ++-- routers/web/repo/issue_stopwatch.go | 2 + routers/web/repo/lfs.go | 3 +- routers/web/repo/migrate.go | 3 +- routers/web/repo/milestone.go | 3 +- routers/web/repo/projects.go | 3 +- routers/web/repo/pull.go | 72 +++++----- routers/web/repo/release.go | 3 +- routers/web/repo/repo.go | 4 +- routers/web/repo/setting.go | 16 +-- routers/web/repo/setting_protected_branch.go | 7 +- routers/web/repo/tag.go | 2 +- routers/web/repo/view.go | 16 +-- routers/web/repo/webhook.go | 11 +- routers/web/repo/wiki.go | 65 +++++++-- routers/web/repo/wiki_test.go | 22 +-- routers/web/user/home.go | 2 +- routers/web/user/notification.go | 5 +- routers/web/user/oauth.go | 2 +- routers/web/user/profile.go | 2 +- routers/web/web.go | 28 ++-- services/lfs/server.go | 7 +- services/webhook/dingtalk.go | 7 +- services/webhook/discord.go | 7 +- services/webhook/general.go | 14 +- services/webhook/matrix.go | 12 +- services/webhook/msteams.go | 7 +- services/wiki/wiki.go | 2 +- templates/admin/emails/list.tmpl | 2 +- templates/admin/repo/list.tmpl | 4 +- templates/admin/user/list.tmpl | 2 +- templates/base/head.tmpl | 4 +- templates/base/head_navbar.tmpl | 13 +- templates/explore/code.tmpl | 6 +- templates/mail/auth/activate.tmpl | 2 +- templates/mail/auth/activate_email.tmpl | 2 +- templates/mail/auth/register_notify.tmpl | 2 +- templates/mail/auth/reset_passwd.tmpl | 2 +- templates/mail/issue/assigned.tmpl | 4 +- templates/mail/issue/default.tmpl | 28 ++-- templates/mail/notify/repo_transfer.tmpl | 2 +- templates/mail/release.tmpl | 10 +- templates/org/home.tmpl | 6 +- templates/org/team/members.tmpl | 4 +- templates/org/team/navbar.tmpl | 4 +- templates/org/team/new.tmpl | 4 +- templates/org/team/repositories.tmpl | 12 +- templates/org/team/sidebar.tmpl | 6 +- templates/org/team/teams.tmpl | 6 +- templates/repo/activity.tmpl | 2 +- templates/repo/blame.tmpl | 8 +- templates/repo/branch/list.tmpl | 30 ++-- templates/repo/branch_dropdown.tmpl | 8 +- templates/repo/commit_page.tmpl | 6 +- templates/repo/commits_list.tmpl | 8 +- templates/repo/commits_list_small.tmpl | 6 +- templates/repo/commits_table.tmpl | 6 +- templates/repo/create.tmpl | 2 +- templates/repo/diff/blob_excerpt.tmpl | 12 +- templates/repo/diff/box.tmpl | 4 +- templates/repo/diff/comments.tmpl | 6 +- templates/repo/diff/compare.tmpl | 38 ++--- templates/repo/diff/image_diff.tmpl | 4 +- templates/repo/diff/options_dropdown.tmpl | 8 +- templates/repo/diff/section_split.tmpl | 6 +- templates/repo/diff/section_unified.tmpl | 6 +- templates/repo/editor/commit_form.tmpl | 2 +- templates/repo/editor/edit.tmpl | 10 +- templates/repo/editor/upload.tmpl | 6 +- templates/repo/forks.tmpl | 4 +- templates/repo/graph/commits.tmpl | 4 +- templates/repo/header.tmpl | 8 +- templates/repo/home.tmpl | 16 +-- templates/repo/issue/labels/label.tmpl | 2 +- templates/repo/issue/list.tmpl | 4 +- templates/repo/issue/view.tmpl | 2 +- templates/repo/issue/view_content.tmpl | 8 +- .../repo/issue/view_content/comments.tmpl | 26 ++-- .../repo/issue/view_content/context_menu.tmpl | 8 +- templates/repo/issue/view_content/pull.tmpl | 10 +- .../repo/issue/view_content/sidebar.tmpl | 26 ++-- templates/repo/issue/view_title.tmpl | 4 +- templates/repo/projects/view.tmpl | 6 +- templates/repo/pulls/commits.tmpl | 2 +- templates/repo/pulls/files.tmpl | 2 +- templates/repo/pulls/fork.tmpl | 4 +- templates/repo/pulls/tab_menu.tmpl | 6 +- templates/repo/release/list.tmpl | 34 ++--- templates/repo/search.tmpl | 8 +- templates/repo/settings/branches.tmpl | 4 +- templates/repo/settings/collaboration.tmpl | 4 +- templates/repo/settings/githooks.tmpl | 2 +- templates/repo/settings/lfs_file.tmpl | 10 +- templates/repo/settings/lfs_file_find.tmpl | 2 +- templates/repo/settings/lfs_locks.tmpl | 4 +- templates/repo/settings/tags.tmpl | 2 +- templates/repo/sub_menu.tmpl | 2 +- templates/repo/view_file.tmpl | 30 ++-- templates/repo/view_list.tmpl | 18 +-- templates/repo/wiki/new.tmpl | 2 +- templates/repo/wiki/pages.tmpl | 2 +- templates/repo/wiki/start.tmpl | 2 +- templates/repo/wiki/view.tmpl | 12 +- templates/shared/issuelist.tmpl | 8 +- templates/user/auth/grant.tmpl | 4 +- templates/user/dashboard/feeds.tmpl | 58 ++++---- templates/user/dashboard/issues.tmpl | 18 +-- templates/user/dashboard/milestones.tmpl | 24 ++-- templates/user/dashboard/navbar.tmpl | 8 +- templates/user/dashboard/repolist.tmpl | 4 +- .../user/notification/notification_div.tmpl | 4 +- templates/user/settings/repos.tmpl | 8 +- web_src/js/features/repo-branch.js | 2 +- 153 files changed, 891 insertions(+), 712 deletions(-) diff --git a/integrations/issue_test.go b/integrations/issue_test.go index 03ca382de..56cddcb06 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -65,7 +65,7 @@ func TestViewIssuesSortByType(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) session := loginUser(t, user.Name) - req := NewRequest(t, "GET", repo.RelLink()+"/issues?type=created_by") + req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -97,7 +97,7 @@ func TestViewIssuesKeyword(t *testing.T) { issues.UpdateIssueIndexer(issue) time.Sleep(time.Second * 1) const keyword = "first" - req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.RelLink(), keyword) + req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) diff --git a/integrations/links_test.go b/integrations/links_test.go index 03229e10e..91166274a 100644 --- a/integrations/links_test.go +++ b/integrations/links_test.go @@ -156,7 +156,7 @@ func testLinksAsUser(userName string, t *testing.T) { "/releases", "/releases/new", //"/wiki/_pages", - "/wiki/_new", + "/wiki/?action=_new", } for _, repo := range apiRepos { diff --git a/integrations/nonascii_branches_test.go b/integrations/nonascii_branches_test.go index 22d71e6ee..cf6261dff 100644 --- a/integrations/nonascii_branches_test.go +++ b/integrations/nonascii_branches_test.go @@ -63,17 +63,17 @@ func TestNonasciiBranches(t *testing.T) { }, { from: "ГлавнаяВетка", - to: "branch/%d0%93%d0%bb%d0%b0%d0%b2%d0%bd%d0%b0%d1%8f%d0%92%d0%b5%d1%82%d0%ba%d0%b0", + to: "branch/%D0%93%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F%D0%92%D0%B5%D1%82%D0%BA%D0%B0", status: http.StatusOK, }, { from: "а/б/в", - to: "branch/%d0%b0/%d0%b1/%d0%b2", + to: "branch/%D0%B0/%D0%B1/%D0%B2", status: http.StatusOK, }, { from: "Grüßen/README.md", - to: "branch/Gr%c3%bc%c3%9fen/README.md", + to: "branch/Gr%C3%BC%C3%9Fen/README.md", status: http.StatusOK, }, { @@ -83,7 +83,7 @@ func TestNonasciiBranches(t *testing.T) { }, { from: "Plus+Is+Not+Space/Файл.md", - to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md", + to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md", status: http.StatusOK, }, { @@ -93,28 +93,28 @@ func TestNonasciiBranches(t *testing.T) { }, { from: "ブランチ", - to: "branch/%e3%83%96%e3%83%a9%e3%83%b3%e3%83%81", + to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81", status: http.StatusOK, }, // Tags { from: "Тэг", - to: "tag/%d0%a2%d1%8d%d0%b3", + to: "tag/%D0%A2%D1%8D%D0%B3", status: http.StatusOK, }, { from: "Ё/人", - to: "tag/%d0%81/%e4%ba%ba", + to: "tag/%D0%81/%E4%BA%BA", status: http.StatusOK, }, { from: "タグ", - to: "tag/%e3%82%bf%e3%82%b0", + to: "tag/%E3%82%BF%E3%82%B0", status: http.StatusOK, }, { from: "タグ/ファイル.md", - to: "tag/%e3%82%bf%e3%82%b0/%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab.md", + to: "tag/%E3%82%BF%E3%82%B0/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md", status: http.StatusOK, }, // Files @@ -125,38 +125,38 @@ func TestNonasciiBranches(t *testing.T) { }, { from: "Файл.md", - to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md", + to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md", status: http.StatusOK, }, { from: "ファイル.md", - to: "branch/Plus+Is+Not+Space/%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab.md", + to: "branch/Plus+Is+Not+Space/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md", status: http.StatusNotFound, // it's not on default branch }, // Same but url-encoded (few tests) { from: "%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81", - to: "branch/%e3%83%96%e3%83%a9%e3%83%b3%e3%83%81", + to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81", status: http.StatusOK, }, { from: "%E3%82%BF%E3%82%b0", - to: "tag/%e3%82%bf%e3%82%b0", + to: "tag/%E3%82%BF%E3%82%B0", status: http.StatusOK, }, { from: "%D0%A4%D0%B0%D0%B9%D0%BB.md", - to: "branch/Plus+Is+Not+Space/%d0%a4%d0%b0%d0%b9%d0%bb.md", + to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md", status: http.StatusOK, }, { from: "%D0%81%2F%E4%BA%BA", - to: "tag/%d0%81/%e4%ba%ba", + to: "tag/%D0%81/%E4%BA%BA", status: http.StatusOK, }, { from: "Ё%2F%E4%BA%BA", - to: "tag/%d0%81/%e4%ba%ba", + to: "tag/%D0%81/%E4%BA%BA", status: http.StatusOK, }, } diff --git a/models/action.go b/models/action.go index 7c970e1fd..80ac3e16f 100644 --- a/models/action.go +++ b/models/action.go @@ -7,6 +7,7 @@ package models import ( "fmt" + "net/url" "path" "strconv" "strings" @@ -185,10 +186,8 @@ func (a *Action) ShortRepoPath() string { // GetRepoLink returns relative link to action repository. func (a *Action) GetRepoLink() string { - if len(setting.AppSubURL) > 0 { - return path.Join(setting.AppSubURL, a.GetRepoPath()) - } - return "/" + a.GetRepoPath() + // path.Join will skip empty strings + return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName()), url.PathEscape(a.GetRepoName())) } // GetRepositoryFromMatch returns a *Repository from a username and repo strings diff --git a/models/attachment.go b/models/attachment.go index ed82aaf48..34edc676c 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -7,6 +7,7 @@ package models import ( "context" "fmt" + "net/url" "path" "code.gitea.io/gitea/models/db" @@ -59,7 +60,7 @@ func (a *Attachment) RelativePath() string { // DownloadURL returns the download url of the attached file func (a *Attachment) DownloadURL() string { - return fmt.Sprintf("%sattachments/%s", setting.AppURL, a.UUID) + return setting.AppURL + "attachments/" + url.PathEscape(a.UUID) } // LinkedRepository returns the linked repo if any diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 0a1445d2f..da63dfd10 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -112,15 +112,15 @@ func GenerateUserAvatarFastLink(userName string, size int) string { if size < 0 { size = 0 } - return setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size) + return setting.AppSubURL + "/user/avatar/" + url.PathEscape(userName) + "/" + strconv.Itoa(size) } // GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}" func GenerateUserAvatarImageLink(userAvatar string, size int) string { if size > 0 { - return setting.AppSubURL + "/avatars/" + userAvatar + "?size=" + strconv.Itoa(size) + return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) + "?size=" + strconv.Itoa(size) } - return setting.AppSubURL + "/avatars/" + userAvatar + return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) } // generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy @@ -155,7 +155,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string { return generateRecognizedAvatarURL(*avatarURL, size) } // for non-final link, we should return fast (use a 302 redirection link) - urlStr := setting.AppSubURL + "/avatar/" + emailHash + urlStr := setting.AppSubURL + "/avatar/" + url.PathEscape(emailHash) if size > 0 { urlStr += "?size=" + strconv.Itoa(size) } diff --git a/models/commit_status.go b/models/commit_status.go index a6ded049c..df34b93ec 100644 --- a/models/commit_status.go +++ b/models/commit_status.go @@ -7,6 +7,7 @@ package models import ( "crypto/sha1" "fmt" + "net/url" "strings" "time" @@ -137,8 +138,7 @@ func (status *CommitStatus) loadAttributes(e db.Engine) (err error) { // APIURL returns the absolute APIURL to this commit-status. func (status *CommitStatus) APIURL() string { _ = status.loadAttributes(db.GetEngine(db.DefaultContext)) - return fmt.Sprintf("%sapi/v1/repos/%s/statuses/%s", - setting.AppURL, status.Repo.FullName(), status.SHA) + return status.Repo.APIURL() + "/statuses/" + url.PathEscape(status.SHA) } // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc diff --git a/models/issue.go b/models/issue.go index 0d34bdcaf..983fb7aa8 100644 --- a/models/issue.go +++ b/models/issue.go @@ -372,6 +372,17 @@ func (issue *Issue) HTMLURL() string { return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) } +// Link returns the Link URL to this issue. +func (issue *Issue) Link() string { + var path string + if issue.IsPull { + path = "pulls" + } else { + path = "issues" + } + return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index) +} + // DiffURL returns the absolute URL to this diff func (issue *Issue) DiffURL() string { if issue.IsPull { diff --git a/models/notification.go b/models/notification.go index 48249ae84..1e1807361 100644 --- a/models/notification.go +++ b/models/notification.go @@ -6,6 +6,7 @@ package models import ( "fmt" + "net/url" "strconv" "code.gitea.io/gitea/models/db" @@ -475,7 +476,7 @@ func (n *Notification) HTMLURL() string { } return n.Issue.HTMLURL() case NotificationSourceCommit: - return n.Repository.HTMLURL() + "/commit/" + n.CommitID + return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID) case NotificationSourceRepository: return n.Repository.HTMLURL() } diff --git a/models/release.go b/models/release.go index 4624791b8..f7bd67b8c 100644 --- a/models/release.go +++ b/models/release.go @@ -10,10 +10,10 @@ import ( "errors" "fmt" "sort" + "strconv" "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -78,23 +78,22 @@ func (r *Release) LoadAttributes() error { // APIURL the api url for a release. release must have attributes loaded func (r *Release) APIURL() string { - return fmt.Sprintf("%sapi/v1/repos/%s/releases/%d", - setting.AppURL, r.Repo.FullName(), r.ID) + return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10) } // ZipURL the zip url for a release. release must have attributes loaded func (r *Release) ZipURL() string { - return fmt.Sprintf("%s/archive/%s.zip", r.Repo.HTMLURL(), r.TagName) + return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".zip" } // TarURL the tar.gz url for a release. release must have attributes loaded func (r *Release) TarURL() string { - return fmt.Sprintf("%s/archive/%s.tar.gz", r.Repo.HTMLURL(), r.TagName) + return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".tar.gz" } // HTMLURL the url for a release on the web UI. release must have attributes loaded func (r *Release) HTMLURL() string { - return fmt.Sprintf("%s/releases/tag/%s", r.Repo.HTMLURL(), r.TagName) + return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) } // IsReleaseExist returns true if release with given tag name already exists. diff --git a/models/repo.go b/models/repo.go index f44fc763a..16396e181 100644 --- a/models/repo.go +++ b/models/repo.go @@ -314,7 +314,7 @@ func (repo *Repository) FullName() string { // HTMLURL returns the repository HTML URL func (repo *Repository) HTMLURL() string { - return setting.AppURL + repo.FullName() + return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } // CommitLink make link to by commit full ID @@ -323,14 +323,14 @@ func (repo *Repository) CommitLink(commitID string) (result string) { if commitID == "" || commitID == "0000000000000000000000000000000000000000" { result = "" } else { - result = repo.HTMLURL() + "/commit/" + commitID + result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID) } return } // APIURL returns the repository API URL func (repo *Repository) APIURL() string { - return setting.AppURL + "api/v1/repos/" + repo.FullName() + return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } // GetCommitsCountCacheKey returns cache key used for commits count caching. @@ -709,19 +709,14 @@ func (repo *Repository) GitConfigPath() string { return GitConfigPath(repo.RepoPath()) } -// RelLink returns the repository relative link -func (repo *Repository) RelLink() string { - return "/" + repo.FullName() -} - // Link returns the repository link func (repo *Repository) Link() string { - return setting.AppSubURL + "/" + repo.FullName() + return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } // ComposeCompareURL returns the repository comparison URL func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string { - return fmt.Sprintf("%s/compare/%s...%s", repo.FullName(), oldCommitID, newCommitID) + return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID)) } // UpdateDefaultBranch updates the default branch @@ -930,11 +925,11 @@ func (repo *Repository) cloneLink(isWiki bool) *CloneLink { } if setting.SSH.Port != 22 { - cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), repo.OwnerName, repoName) + cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) } else if setting.Repository.UseCompatSSHURI { - cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, repo.OwnerName, repoName) + cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) } else { - cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, repo.OwnerName, repoName) + cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName)) } cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) return cl diff --git a/models/repo_avatar.go b/models/repo_avatar.go index 6c5e03c0d..aa1b3bc15 100644 --- a/models/repo_avatar.go +++ b/models/repo_avatar.go @@ -10,6 +10,7 @@ import ( "fmt" "image/png" "io" + "net/url" "strconv" "strings" @@ -96,7 +97,7 @@ func (repo *Repository) relAvatarLink(e db.Engine) string { return "" } } - return setting.AppSubURL + "/repo-avatars/" + repo.Avatar + return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar) } // AvatarLink returns a link to the repository's avatar. diff --git a/models/user.go b/models/user.go index 12035dbe4..8146c184e 100644 --- a/models/user.go +++ b/models/user.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" _ "image/jpeg" // Needed for jpeg support + "net/url" "os" "path/filepath" "regexp" @@ -315,17 +316,17 @@ func (u *User) DashboardLink() string { // HomeLink returns the user or organization home page link. func (u *User) HomeLink() string { - return setting.AppSubURL + "/" + u.Name + return setting.AppSubURL + "/" + url.PathEscape(u.Name) } // HTMLURL returns the user or organization's full link. func (u *User) HTMLURL() string { - return setting.AppURL + u.Name + return setting.AppURL + url.PathEscape(u.Name) } // OrganisationLink returns the organization sub page link. func (u *User) OrganisationLink() string { - return setting.AppSubURL + "/org/" + u.Name + return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) } // GenerateEmailActivateCode generates an activate code based on user information and given e-mail. diff --git a/modules/context/context.go b/modules/context/context.go index cb7131907..8adf1f306 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -70,6 +70,16 @@ type Context struct { Org *Organization } +// TrHTMLEscapeArgs runs Tr but pre-escapes all arguments with html.EscapeString. +// This is useful if the locale message is intended to only produce HTML content. +func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { + trArgs := make([]interface{}, len(args)) + for i, arg := range args { + trArgs[i] = html.EscapeString(arg) + } + return ctx.Tr(msg, trArgs...) +} + // GetData returns the data func (ctx *Context) GetData() map[string]interface{} { return ctx.Data @@ -120,9 +130,9 @@ func RedirectToUser(ctx *Context, userName string, redirectUserID int64) { } redirectPath := strings.Replace( - ctx.Req.URL.Path, - userName, - user.Name, + ctx.Req.URL.EscapedPath(), + url.PathEscape(userName), + url.PathEscape(user.Name), 1, ) if ctx.Req.URL.RawQuery != "" { diff --git a/modules/context/repo.go b/modules/context/repo.go index d5763c78a..3be33f248 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -41,10 +41,10 @@ var IssueTemplateDirCandidates = []string{ // PullRequest contains information to make a pull request type PullRequest struct { - BaseRepo *models.Repository - Allowed bool - SameRepo bool - HeadInfo string // [:] + BaseRepo *models.Repository + Allowed bool + SameRepo bool + HeadInfoSubURL string // [:] url segment } // Repository contains information to operate a repository @@ -189,11 +189,11 @@ func (r *Repository) GetCommitGraphsCount(hidePRRefs bool, branches []string, fi func (r *Repository) BranchNameSubURL() string { switch { case r.IsViewBranch: - return "branch/" + r.BranchName + return "branch/" + util.PathEscapeSegments(r.BranchName) case r.IsViewTag: - return "tag/" + r.BranchName + return "tag/" + util.PathEscapeSegments(r.BranchName) case r.IsViewCommit: - return "commit/" + r.BranchName + return "commit/" + util.PathEscapeSegments(r.BranchName) } log.Error("Unknown view type for repo: %v", r) return "" @@ -321,9 +321,9 @@ func RedirectToRepo(ctx *Context, redirectRepoID int64) { } redirectPath := strings.Replace( - ctx.Req.URL.Path, - fmt.Sprintf("%s/%s", ownerName, previousRepoName), - repo.FullName(), + ctx.Req.URL.EscapedPath(), + url.PathEscape(ownerName)+"/"+url.PathEscape(previousRepoName), + url.PathEscape(repo.OwnerName)+"/"+url.PathEscape(repo.Name), 1, ) if ctx.Req.URL.RawQuery != "" { @@ -588,7 +588,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["BaseRepo"] = repo.BaseRepo ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo ctx.Repo.PullRequest.Allowed = canPush - ctx.Repo.PullRequest.HeadInfo = ctx.Repo.Owner.Name + ":" + ctx.Repo.BranchName + ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Repo.Owner.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } else if repo.AllowsPulls() { // Or, this is repository accepts pull requests between branches. canCompare = true @@ -596,7 +596,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Repo.PullRequest.BaseRepo = repo ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.SameRepo = true - ctx.Repo.PullRequest.HeadInfo = ctx.Repo.BranchName + ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName) } ctx.Data["CanCompareOrPull"] = canCompare ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest @@ -621,7 +621,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { if ctx.FormString("go-get") == "1" { ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name) - prefix := setting.AppURL + path.Join(owner.Name, repo.Name, "src", "branch", ctx.Repo.BranchName) + prefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) ctx.Data["GoDocDirectory"] = prefix + "{/dir}" ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}" } @@ -810,7 +810,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context if isRenamedBranch && has { renamedBranchName := ctx.Data["RenamedBranchName"].(string) ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) - link := strings.Replace(ctx.Req.RequestURI, refName, renamedBranchName, 1) + link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1) ctx.Redirect(link) return } @@ -845,7 +845,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context // If short commit ID add canonical link header if len(refName) < 40 { ctx.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"", - util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), refName, ctx.Repo.Commit.ID.String(), 1)))) + util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)))) } } else { if len(ignoreNotExistErr) > 0 && ignoreNotExistErr[0] { @@ -857,11 +857,13 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context if refType == RepoRefLegacy { // redirect from old URL scheme to new URL scheme + prefix := strings.TrimPrefix(setting.AppSubURL+strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*")), ctx.Repo.RepoLink) + ctx.Redirect(path.Join( - setting.AppSubURL, - strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*")), + ctx.Repo.RepoLink, + util.PathEscapeSegments(prefix), ctx.Repo.BranchNameSubURL(), - ctx.Repo.TreePath)) + util.PathEscapeSegments(ctx.Repo.TreePath))) return } } diff --git a/modules/convert/git_commit.go b/modules/convert/git_commit.go index 9f43bb82f..9905b51fe 100644 --- a/modules/convert/git_commit.go +++ b/modules/convert/git_commit.go @@ -5,6 +5,7 @@ package convert import ( + "net/url" "time" "code.gitea.io/gitea/models" @@ -126,7 +127,7 @@ func ToCommit(repo *models.Repository, commit *git.Commit, userCache map[string] for i := 0; i < commit.ParentCount(); i++ { sha, _ := commit.ParentID(i) apiParents[i] = &api.CommitMeta{ - URL: repo.APIURL() + "/git/commits/" + sha.String(), + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(sha.String()), SHA: sha.String(), } } @@ -147,13 +148,13 @@ func ToCommit(repo *models.Repository, commit *git.Commit, userCache map[string] return &api.Commit{ CommitMeta: &api.CommitMeta{ - URL: repo.APIURL() + "/git/commits/" + commit.ID.String(), + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), SHA: commit.ID.String(), Created: commit.Committer.When, }, - HTMLURL: repo.HTMLURL() + "/commit/" + commit.ID.String(), + HTMLURL: repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()), RepoCommit: &api.RepoCommit{ - URL: repo.APIURL() + "/git/commits/" + commit.ID.String(), + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), Author: &api.CommitUser{ Identity: api.Identity{ Name: commit.Author.Name, @@ -170,7 +171,7 @@ func ToCommit(repo *models.Repository, commit *git.Commit, userCache map[string] }, Message: commit.Message(), Tree: &api.CommitMeta{ - URL: repo.APIURL() + "/git/trees/" + commit.ID.String(), + URL: repo.APIURL() + "/git/trees/" + url.PathEscape(commit.ID.String()), SHA: commit.ID.String(), Created: commit.Committer.When, }, diff --git a/modules/convert/issue.go b/modules/convert/issue.go index 3974d460e..7363cfb8f 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -6,6 +6,7 @@ package convert import ( "fmt" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -191,7 +192,7 @@ func ToLabel(label *models.Label, repo *models.Repository, org *models.User) *ap } } else { // BelongsToOrg if org != nil { - result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, org.Name, label.ID) + result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID) } else { log.Error("ToLabel did not get org to calculate url for label with id '%d'", label.ID) } diff --git a/modules/convert/notification.go b/modules/convert/notification.go index fae7be125..5f4fef02b 100644 --- a/modules/convert/notification.go +++ b/modules/convert/notification.go @@ -5,6 +5,8 @@ package convert import ( + "net/url" + "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" ) @@ -58,7 +60,7 @@ func ToNotificationThread(n *models.Notification) *api.NotificationThread { } } case models.NotificationSourceCommit: - url := n.Repository.HTMLURL() + "/commit/" + n.CommitID + url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID) result.Subject = &api.NotificationSubject{ Type: api.NotifySubjectCommit, Title: n.CommitID, diff --git a/modules/git/utils.go b/modules/git/utils.go index 13926fba7..6988f31a3 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -11,6 +11,8 @@ import ( "strconv" "strings" "sync" + + "code.gitea.io/gitea/modules/util" ) // ObjectCache provides thread-safe cache operations. @@ -92,7 +94,7 @@ func RefEndName(refStr string) string { // RefURL returns the absolute URL for a ref in a repository func RefURL(repoURL, ref string) string { - refName := RefEndName(ref) + refName := util.PathEscapeSegments(RefEndName(ref)) switch { case strings.HasPrefix(ref, BranchPrefix): return repoURL + "/src/branch/" + refName diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index d7e3ff452..0bcdb8c3a 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -7,6 +7,7 @@ package repofiles import ( "fmt" "html" + "net/url" "regexp" "strconv" "strings" @@ -175,7 +176,7 @@ func UpdateIssuesCommit(doer *models.User, repo *models.Repository, commits []*r continue } - message := fmt.Sprintf(`%s`, repo.Link(), c.Sha1, html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0])) + message := fmt.Sprintf(`%s`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0])) if err = models.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil { return err } diff --git a/modules/repofiles/blob.go b/modules/repofiles/blob.go index 60a05e280..02bc1ebca 100644 --- a/modules/repofiles/blob.go +++ b/modules/repofiles/blob.go @@ -5,6 +5,8 @@ package repofiles import ( + "net/url" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" @@ -31,7 +33,7 @@ func GetBlobBySHA(repo *models.Repository, sha string) (*api.GitBlobResponse, er } return &api.GitBlobResponse{ SHA: gitBlob.ID.String(), - URL: repo.APIURL() + "/git/blobs/" + gitBlob.ID.String(), + URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()), Size: gitBlob.Size(), Encoding: "base64", Content: content, diff --git a/modules/repofiles/file.go b/modules/repofiles/file.go index abd14b1db..403092401 100644 --- a/modules/repofiles/file.go +++ b/modules/repofiles/file.go @@ -36,19 +36,19 @@ func GetFileCommitResponse(repo *models.Repository, commit *git.Commit) (*api.Fi if commit == nil { return nil, fmt.Errorf("commit cannot be nil") } - commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + commit.ID.String()) - commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + commit.Tree.ID.String()) + commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String())) + commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String())) parents := make([]*api.CommitMeta, commit.ParentCount()) for i := 0; i <= commit.ParentCount(); i++ { if parent, err := commit.Parent(i); err == nil && parent != nil { - parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + parent.ID.String()) + parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(parent.ID.String())) parents[i] = &api.CommitMeta{ SHA: parent.ID.String(), URL: parentCommitURL.String(), } } } - commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + commit.ID.String()) + commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String())) fileCommit := &api.FileCommitResponse{ CommitMeta: api.CommitMeta{ SHA: commit.ID.String(), diff --git a/modules/repofiles/tree.go b/modules/repofiles/tree.go index b3edea341..81579dccc 100644 --- a/modules/repofiles/tree.go +++ b/modules/repofiles/tree.go @@ -6,6 +6,7 @@ package repofiles import ( "fmt" + "net/url" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" @@ -28,7 +29,7 @@ func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recurs } tree := new(api.GitTreeResponse) tree.SHA = gitTree.ResolvedID.String() - tree.URL = repo.APIURL() + "/git/trees/" + tree.SHA + tree.URL = repo.APIURL() + "/git/trees/" + url.PathEscape(tree.SHA) var entries git.Entries if recursive { entries, err = gitTree.ListEntriesRecursive() diff --git a/modules/repository/commits.go b/modules/repository/commits.go index c86f0d570..a545ce952 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -6,6 +6,7 @@ package repository import ( "fmt" + "net/url" "time" "code.gitea.io/gitea/models" @@ -81,7 +82,7 @@ func (pc *PushCommits) toAPIPayloadCommit(repoPath, repoLink string, commit *Pus return &api.PayloadCommit{ ID: commit.Sha1, Message: commit.Message, - URL: fmt.Sprintf("%s/commit/%s", repoLink, commit.Sha1), + URL: fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(commit.Sha1)), Author: &api.PayloadUser{ Name: commit.AuthorName, Email: commit.AuthorEmail, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 991816c10..8b46ed40c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -139,17 +139,14 @@ func NewFuncMap() []template.FuncMap { } return str[start:end] }, - "EllipsisString": base.EllipsisString, - "DiffTypeToStr": DiffTypeToStr, - "DiffLineTypeToStr": DiffLineTypeToStr, - "Sha1": Sha1, - "ShortSha": base.ShortSha, - "MD5": base.EncodeMD5, - "ActionContent2Commits": ActionContent2Commits, - "PathEscape": url.PathEscape, - "EscapePound": func(str string) string { - return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str) - }, + "EllipsisString": base.EllipsisString, + "DiffTypeToStr": DiffTypeToStr, + "DiffLineTypeToStr": DiffLineTypeToStr, + "Sha1": Sha1, + "ShortSha": base.ShortSha, + "MD5": base.EncodeMD5, + "ActionContent2Commits": ActionContent2Commits, + "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, "URLJoin": util.URLJoin, "RenderCommitMessage": RenderCommitMessage, @@ -742,7 +739,7 @@ func ReactionToEmoji(reaction string) template.HTML { if val != nil { return template.HTML(val.Emoji) } - return template.HTML(fmt.Sprintf(`:%s:`, reaction, setting.StaticURLPrefix, reaction)) + return template.HTML(fmt.Sprintf(`:%s:`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) } // RenderNote renders the contents of a git-notes file as a commit message. diff --git a/modules/upload/upload.go b/modules/upload/upload.go index e430a5d8b..097facb4d 100644 --- a/modules/upload/upload.go +++ b/modules/upload/upload.go @@ -6,6 +6,7 @@ package upload import ( "net/http" + "net/url" "path" "regexp" "strings" @@ -83,7 +84,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" if len(ctx.Params(":index")) > 0 { - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + ctx.Params(":index") + "/attachments" + ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.Params(":index")) + "/attachments" } else { ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/attachments" } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index af6887640..c39063e46 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1409,7 +1409,7 @@ pulls.filter_branch = Filter branch pulls.no_results = No results found. pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. -pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` +pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` pulls.create = Create Pull Request pulls.title_desc = wants to merge %[1]d commits from %[2]s into %[3]s pulls.merged_title_desc = merged %[1]d commits from %[2]s into %[3]s %[4]s @@ -2761,32 +2761,32 @@ notices.delete_success = The system notices have been deleted. [action] create_repo = created repository %s rename_repo = renamed repository from %[1]s to %[3]s -commit_repo = pushed to %[3]s at %[4]s -create_issue = `opened issue %s#%[2]s` -close_issue = `closed issue %s#%[2]s` -reopen_issue = `reopened issue %s#%[2]s` -create_pull_request = `created pull request %s#%[2]s` -close_pull_request = `closed pull request %s#%[2]s` -reopen_pull_request = `reopened pull request %s#%[2]s` -comment_issue = `commented on issue %s#%[2]s` -comment_pull = `commented on pull request %s#%[2]s` -merge_pull_request = `merged pull request %s#%[2]s` +commit_repo = pushed to %[3]s at %[4]s +create_issue = `opened issue %[3]s#%[2]s` +close_issue = `closed issue %[3]s#%[2]s` +reopen_issue = `reopened issue %[3]s#%[2]s` +create_pull_request = `created pull request %[3]s#%[2]s` +close_pull_request = `closed pull request %[3]s#%[2]s` +reopen_pull_request = `reopened pull request %[3]s#%[2]s` +comment_issue = `commented on issue %[3]s#%[2]s` +comment_pull = `commented on pull request %[3]s#%[2]s` +merge_pull_request = `merged pull request %[3]s#%[2]s` transfer_repo = transferred repository %s to %s -push_tag = pushed tag %[4]s to %[3]s +push_tag = pushed tag %[3]s to %[4]s delete_tag = deleted tag %[2]s from %[3]s delete_branch = deleted branch %[2]s from %[3]s compare_branch = Compare compare_commits = Compare %d commits compare_commits_general = Compare commits -mirror_sync_push = synced commits to %[3]s at %[4]s from mirror -mirror_sync_create = synced new reference %[2]s to %[3]s from mirror +mirror_sync_push = synced commits to %[3]s at %[4]s from mirror +mirror_sync_create = synced new reference %[3]s to %[4]s from mirror mirror_sync_delete = synced and deleted reference %[2]s at %[3]s from mirror -approve_pull_request = `approved %s#%[2]s` -reject_pull_request = `suggested changes for %s#%[2]s` -publish_release = `released "%[4]s" at %[3]s` -review_dismissed = `dismissed review from %[4]s for %[3]s#%[2]s` +approve_pull_request = `approved %[3]s#%[2]s` +reject_pull_request = `suggested changes for %[3]s#%[2]s` +publish_release = `released "%[4]s" at %[3]s` +review_dismissed = `dismissed review from %[4]s for %[3]s#%[2]s` review_dismissed_reason = Reason: -create_branch = created branch %[3]s in %[4]s +create_branch = created branch %[3]s in %[4]s starred_repo = starred %[2]s watched_repo = started watching %[2]s diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index a6f140c38..4530349f2 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -6,6 +6,7 @@ package org import ( "net/http" + "net/url" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -159,7 +160,7 @@ func IsMember(ctx *context.APIContext) { } } - redirectURL := setting.AppSubURL + "/api/v1/orgs/" + ctx.Org.Organization.Name + "/public_members/" + userToCheck.Name + redirectURL := setting.AppSubURL + "/api/v1/orgs/" + url.PathEscape(ctx.Org.Organization.Name) + "/public_members/" + url.PathEscape(userToCheck.Name) ctx.Redirect(redirectURL, 302) } diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go index e304e0674..29b126db9 100644 --- a/routers/api/v1/repo/git_ref.go +++ b/routers/api/v1/repo/git_ref.go @@ -6,9 +6,11 @@ package repo import ( "net/http" + "net/url" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" ) @@ -89,11 +91,11 @@ func getGitRefsInternal(ctx *context.APIContext, filter string) { for i := range refs { apiRefs[i] = &api.Reference{ Ref: refs[i].Name, - URL: ctx.Repo.Repository.APIURL() + "/git/" + refs[i].Name, + URL: ctx.Repo.Repository.APIURL() + "/git/" + util.PathEscapeSegments(refs[i].Name), Object: &api.GitObject{ SHA: refs[i].Object.String(), Type: refs[i].Type, - URL: ctx.Repo.Repository.APIURL() + "/git/" + refs[i].Type + "s/" + refs[i].Object.String(), + URL: ctx.Repo.Repository.APIURL() + "/git/" + url.PathEscape(refs[i].Type) + "s/" + url.PathEscape(refs[i].Object.String()), }, } } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 98ee2b4de..c20a4776c 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -8,6 +8,7 @@ package repo import ( "fmt" "net/http" + "net/url" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -33,8 +34,8 @@ func appendPrivateInformation(apiKey *api.DeployKey, key *models.DeployKey, repo return apiKey, nil } -func composeDeployKeysAPILink(repoPath string) string { - return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/" +func composeDeployKeysAPILink(owner, name string) string { + return setting.AppURL + "api/v1/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/keys/" } // ListDeployKeys list all the deploy keys of a repository @@ -94,7 +95,7 @@ func ListDeployKeys(ctx *context.APIContext) { return } - apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) + apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) apiKeys := make([]*api.DeployKey, len(keys)) for i := range keys { if err := keys[i].GetContent(); err != nil { @@ -154,7 +155,7 @@ func GetDeployKey(ctx *context.APIContext) { return } - apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) + apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) apiKey := convert.ToDeployKey(apiLink, key) if ctx.User.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.User.ID == ctx.Repo.Owner.ID)) { apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Repo.Repository) @@ -233,7 +234,7 @@ func CreateDeployKey(ctx *context.APIContext) { } key.Content = content - apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) + apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) ctx.JSON(http.StatusCreated, convert.ToDeployKey(apiLink, key)) } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 460b74017..5fd15b5c5 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -8,7 +8,9 @@ import ( "errors" "fmt" "net/http" + "net/url" "regexp" + "strconv" "code.gitea.io/gitea/models/login" "code.gitea.io/gitea/modules/auth/pam" @@ -396,7 +398,7 @@ func EditAuthSourcePost(ctx *context.Context) { log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID) ctx.Flash.Success(ctx.Tr("admin.auths.update_success")) - ctx.Redirect(setting.AppSubURL + "/admin/auths/" + fmt.Sprint(form.ID)) + ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10)) } // DeleteAuthSource response for deleting an auth source @@ -414,7 +416,7 @@ func DeleteAuthSource(ctx *context.Context) { ctx.Flash.Error(fmt.Sprintf("DeleteLoginSource: %v", err)) } ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": setting.AppSubURL + "/admin/auths/" + ctx.Params(":authid"), + "redirect": setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")), }) return } diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index a13f7317e..432dd2f6a 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -58,7 +58,7 @@ func DeleteRepo(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.FormString("page") + "&sort=" + ctx.FormString("sort"), + "redirect": setting.AppSubURL + "/admin/repos?page=" + url.QueryEscape(ctx.FormString("page")) + "&sort=" + url.QueryEscape(ctx.FormString("sort")), }) } @@ -161,5 +161,5 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir)) } - ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + page) + ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + url.QueryEscape(page)) } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index db7fe7b36..8bafd1f19 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -6,8 +6,8 @@ package admin import ( - "fmt" "net/http" + "net/url" "strconv" "strings" @@ -188,7 +188,7 @@ func NewUserPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name)) - ctx.Redirect(setting.AppSubURL + "/admin/users/" + fmt.Sprint(u.ID)) + ctx.Redirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10)) } func prepareUserInfo(ctx *context.Context) *models.User { @@ -366,7 +366,7 @@ func EditUserPost(ctx *context.Context) { log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name) ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success")) - ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid")) + ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) } // DeleteUser response for deleting a user @@ -382,12 +382,12 @@ func DeleteUser(ctx *context.Context) { case models.IsErrUserOwnRepos(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo")) ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"), + "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")), }) case models.IsErrUserHasOrgs(err): ctx.Flash.Error(ctx.Tr("admin.users.still_has_org")) ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"), + "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")), }) default: ctx.ServerError("DeleteUser", err) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index dfb03785a..474366862 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,10 +15,35 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "github.com/gorilla/feeds" ) +func toBranchLink(act *models.Action) string { + return act.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(act.GetBranch()) +} + +func toTagLink(act *models.Action) string { + return act.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(act.GetTag()) +} + +func toIssueLink(act *models.Action) string { + return act.GetRepoLink() + "/issues/" + url.PathEscape(act.GetIssueInfos()[0]) +} + +func toPullLink(act *models.Action) string { + return act.GetRepoLink() + "/pulls/" + url.PathEscape(act.GetIssueInfos()[0]) +} + +func toSrcLink(act *models.Action) string { + return act.GetRepoLink() + "/src/" + util.PathEscapeSegments(act.GetBranch()) +} + +func toReleaseLink(act *models.Action) string { + return act.GetRepoLink() + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch()) +} + // feedActionsToFeedItems convert gitea's Action feed to feeds Item func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (items []*feeds.Item, err error) { for _, act := range actions { @@ -32,62 +57,111 @@ func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (ite title = act.ActUser.DisplayName() + " " switch act.OpType { case models.ActionCreateRepo: - title += ctx.Tr("action.create_repo", act.GetRepoLink(), act.ShortRepoPath()) + title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoLink(), act.ShortRepoPath()) + link.Href = act.GetRepoLink() case models.ActionRenameRepo: - title += ctx.Tr("action.rename_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) + title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) + link.Href = act.GetRepoLink() case models.ActionCommitRepo: - branchLink := act.GetBranch() + link.Href = toBranchLink(act) if len(act.Content) != 0 { - title += ctx.Tr("action.commit_repo", act.GetRepoLink(), branchLink, act.GetBranch(), act.ShortRepoPath()) + title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoLink(), link.Href, act.GetBranch(), act.ShortRepoPath()) } else { - title += ctx.Tr("action.create_branch", act.GetRepoLink(), branchLink, act.GetBranch(), act.ShortRepoPath()) + title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoLink(), link.Href, act.GetBranch(), act.ShortRepoPath()) } case models.ActionCreateIssue: - title += ctx.Tr("action.create_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + link.Href = toIssueLink(act) + title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionCreatePullRequest: - title += ctx.Tr("action.create_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + link.Href = toPullLink(act) + title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionTransferRepo: - title += ctx.Tr("action.transfer_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) case models.ActionPushTag: - title += ctx.Tr("action.push_tag", act.GetRepoLink(), url.QueryEscape(act.GetTag()), act.ShortRepoPath()) + link.Href = toTagLink(act) + title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoLink(), link.Href, act.GetTag(), act.ShortRepoPath()) case models.ActionCommentIssue: - title += ctx.Tr("action.comment_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + issueLink := toIssueLink(act) + if link.Href == "#" { + link.Href = issueLink + } + title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionMergePullRequest: - title += ctx.Tr("action.merge_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + pullLink := toPullLink(act) + if link.Href == "#" { + link.Href = pullLink + } + title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionCloseIssue: - title += ctx.Tr("action.close_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + issueLink := toIssueLink(act) + if link.Href == "#" { + link.Href = issueLink + } + title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionReopenIssue: - title += ctx.Tr("action.reopen_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + issueLink := toIssueLink(act) + if link.Href == "#" { + link.Href = issueLink + } + title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionClosePullRequest: - title += ctx.Tr("action.close_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + pullLink := toPullLink(act) + if link.Href == "#" { + link.Href = pullLink + } + title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionReopenPullRequest: - title += ctx.Tr("action.reopen_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath) + pullLink := toPullLink(act) + if link.Href == "#" { + link.Href = pullLink + } + title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionDeleteTag: - title += ctx.Tr("action.delete_tag", act.GetRepoLink(), html.EscapeString(act.GetTag()), act.ShortRepoPath()) + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoLink(), act.GetTag(), act.ShortRepoPath()) case models.ActionDeleteBranch: - title += ctx.Tr("action.delete_branch", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) case models.ActionMirrorSyncPush: - title += ctx.Tr("action.mirror_sync_push", act.GetRepoLink(), url.QueryEscape(act.GetBranch()), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) + srcLink := toSrcLink(act) + if link.Href == "#" { + link.Href = srcLink + } + title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoLink(), srcLink, act.GetBranch(), act.ShortRepoPath()) case models.ActionMirrorSyncCreate: - title += ctx.Tr("action.mirror_sync_create", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) + srcLink := toSrcLink(act) + if link.Href == "#" { + link.Href = srcLink + } + title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoLink(), srcLink, act.GetBranch(), act.ShortRepoPath()) case models.ActionMirrorSyncDelete: - title += ctx.Tr("action.mirror_sync_delete", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoLink(), act.GetBranch(), act.ShortRepoPath()) case models.ActionApprovePullRequest: - title += ctx.Tr("action.approve_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + pullLink := toPullLink(act) + title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionRejectPullRequest: - title += ctx.Tr("action.reject_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + pullLink := toPullLink(act) + title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionCommentPull: - title += ctx.Tr("action.comment_pull", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) + pullLink := toPullLink(act) + title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case models.ActionPublishRelease: - title += ctx.Tr("action.publish_release", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath(), act.Content) + releaseLink := toReleaseLink(act) + if link.Href == "#" { + link.Href = releaseLink + } + title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoLink(), releaseLink, act.ShortRepoPath(), act.Content) case models.ActionPullReviewDismissed: - title += ctx.Tr("action.review_dismissed", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath(), act.GetIssueInfos()[1]) + pullLink := toPullLink(act) + title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(), act.GetIssueInfos()[1]) case models.ActionStarRepo: - title += ctx.Tr("action.starred_repo", act.GetRepoLink(), act.GetRepoPath()) - link = &feeds.Link{Href: act.GetRepoLink()} + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoLink(), act.GetRepoPath()) case models.ActionWatchRepo: - title += ctx.Tr("action.watched_repo", act.GetRepoLink(), act.GetRepoPath()) - link = &feeds.Link{Href: act.GetRepoLink()} + link.Href = act.GetRepoLink() + title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoLink(), act.GetRepoPath()) default: return nil, fmt.Errorf("unknown action type: %v", act.OpType) } @@ -104,7 +178,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (ite desc += "\n\n" } desc += fmt.Sprintf("%s\n%s", - fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), commit.Sha1), + html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), commit.Sha1)), commit.Sha1, templates.RenderCommitMessage(commit.Message, repoLink, nil), ) diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index d8add77f6..53c31a1c6 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -7,6 +7,7 @@ package org import ( "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -76,7 +77,7 @@ func SettingsPost(ctx *context.Context) { return } // reset ctx.org.OrgLink with new name - ctx.Org.OrgLink = setting.AppSubURL + "/org/" + form.Name + ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name) log.Trace("Organization name changed: %s -> %s", org.Name, form.Name) nameChanged = false } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 3fe97142a..2fc72f062 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -7,6 +7,7 @@ package org import ( "net/http" + "net/url" "path" "strings" @@ -105,7 +106,7 @@ func TeamsAction(ctx *context.Context) { } ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName, + "redirect": ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName), }) return case "add": @@ -119,7 +120,7 @@ func TeamsAction(ctx *context.Context) { if err != nil { if models.IsErrUserNotExist(err) { ctx.Flash.Error(ctx.Tr("form.user_not_exist")) - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) } else { ctx.ServerError(" GetUserByName", err) } @@ -128,7 +129,7 @@ func TeamsAction(ctx *context.Context) { if u.IsOrganization() { ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team")) - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) return } @@ -156,7 +157,7 @@ func TeamsAction(ctx *context.Context) { switch page { case "team": - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) case "home": ctx.Redirect(ctx.Org.Organization.HomeLink()) default: @@ -181,7 +182,7 @@ func TeamsRepoAction(ctx *context.Context) { if err != nil { if models.IsErrRepoNotExist(err) { ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo")) - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories") + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") return } ctx.ServerError("GetRepositoryByName", err) @@ -204,11 +205,11 @@ func TeamsRepoAction(ctx *context.Context) { if action == "addall" || action == "removeall" { ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories", + "redirect": ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories", }) return } - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories") + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") } // NewTeam render create new team page @@ -273,7 +274,7 @@ func NewTeamPost(ctx *context.Context) { return } log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name) - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) } // TeamMembers render team members page @@ -375,7 +376,7 @@ func EditTeamPost(ctx *context.Context) { } return } - ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) } // DeleteTeam response for the delete team request diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 3632d1846..110ec037e 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -8,6 +8,7 @@ import ( "fmt" gotemplate "html/template" "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" ) const ( @@ -54,7 +56,7 @@ func RefBlame(ctx *context.Context) { rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + ctx.Repo.TreePath + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } var treeNames []string @@ -85,7 +87,7 @@ func RefBlame(ctx *context.Context) { ctx.Data["TreeNames"] = treeNames ctx.Data["BranchLink"] = branchLink - ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath + ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) ctx.Data["PageIsViewCode"] = true ctx.Data["IsBlame"] = true @@ -236,8 +238,8 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m br.RepoLink = repoLink br.PartSha = part.Sha br.PreviousSha = previousSha - br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, previousSha, ctx.Repo.TreePath) - br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, part.Sha) + br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(previousSha), util.PathEscapeSegments(ctx.Repo.TreePath)) + br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha)) br.CommitMessage = commit.CommitMessage br.CommitSince = commitSince } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 4c0f94f15..06cce9241 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -8,7 +8,6 @@ package repo import ( "errors" "net/http" - "path" "strings" "code.gitea.io/gitea/models" @@ -323,8 +322,7 @@ func Diff(ctx *context.Context) { return } } - headTarget := path.Join(userName, repoName) - setCompareContext(ctx, parentCommit, commit, headTarget) + setCompareContext(ctx, parentCommit, commit, userName, repoName) ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) ctx.Data["Commit"] = commit ctx.Data["Diff"] = diff diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 86ecc2bab..01c324e9e 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -12,7 +12,7 @@ import ( "html" "io" "net/http" - "path" + "net/url" "path/filepath" "strings" @@ -38,7 +38,7 @@ const ( ) // setCompareContext sets context data. -func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) { +func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headOwner, headName string) { ctx.Data["BaseCommit"] = base ctx.Data["HeadCommit"] = head @@ -54,22 +54,28 @@ func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, return blob } - setPathsCompareContext(ctx, base, head, headTarget) + setPathsCompareContext(ctx, base, head, headOwner, headName) setImageCompareContext(ctx) setCsvCompareContext(ctx) } -// setPathsCompareContext sets context data for source and raw paths -func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) { - sourcePath := setting.AppSubURL + "/%s/src/commit/%s" - rawPath := setting.AppSubURL + "/%s/raw/commit/%s" +// SourceCommitURL creates a relative URL for a commit in the given repository +func SourceCommitURL(owner, name string, commit *git.Commit) string { + return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/src/commit/" + url.PathEscape(commit.ID.String()) +} - ctx.Data["SourcePath"] = fmt.Sprintf(sourcePath, headTarget, head.ID) - ctx.Data["RawPath"] = fmt.Sprintf(rawPath, headTarget, head.ID) +// RawCommitURL creates a relative URL for the raw commit in the given repository +func RawCommitURL(owner, name string, commit *git.Commit) string { + return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/raw/commit/" + url.PathEscape(commit.ID.String()) +} + +// setPathsCompareContext sets context data for source and raw paths +func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headOwner, headName string) { + ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head) + ctx.Data["RawPath"] = RawCommitURL(headOwner, headName, head) if base != nil { - baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) - ctx.Data["BeforeSourcePath"] = fmt.Sprintf(sourcePath, baseTarget, base.ID) - ctx.Data["BeforeRawPath"] = fmt.Sprintf(rawPath, baseTarget, base.ID) + ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, head) + ctx.Data["BeforeRawPath"] = RawCommitURL(headOwner, headName, head) } } @@ -619,8 +625,7 @@ func PrepareCompareDiff( ctx.Data["Username"] = ci.HeadUser.Name ctx.Data["Reponame"] = ci.HeadRepo.Name - headTarget := path.Join(ci.HeadUser.Name, repo.Name) - setCompareContext(ctx, baseCommit, headCommit, headTarget) + setCompareContext(ctx, baseCommit, headCommit, ci.HeadUser.Name, repo.Name) return false } diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index d9f8c2009..088edbfd2 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -204,7 +204,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["TreePath"] = form.TreePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) ctx.Data["FileContent"] = form.Content ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage @@ -299,9 +299,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Error(http.StatusInternalServerError, err.Error()) } } else if models.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form) + ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form) } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form) + ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form) } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) if len(errPushRej.Message) == 0 { @@ -495,7 +495,7 @@ func DeleteFilePost(ctx *context.Context) { ctx.Error(http.StatusInternalServerError, err.Error()) } } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form) + ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) if len(errPushRej.Message) == 0 { @@ -602,7 +602,7 @@ func UploadFilePost(ctx *context.Context) { ctx.Data["TreePath"] = form.TreePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage ctx.Data["commit_choice"] = form.CommitChoice @@ -698,7 +698,7 @@ func UploadFilePost(ctx *context.Context) { branchErr := err.(models.ErrBranchAlreadyExists) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+ctx.Repo.CommitID+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) + ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) if len(errPushRej.Message) == 0 { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d9e15a784..95363258e 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "net/url" "path" "strconv" "strings" @@ -106,7 +107,7 @@ func MustAllowPulls(ctx *context.Context) { // User can send pull request if owns a forked repository. if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) { ctx.Repo.PullRequest.Allowed = true - ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName + ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.User.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } } @@ -764,7 +765,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs [ for _, repoLabel := range repoLabels { if strings.EqualFold(repoLabel.Name, metaLabel) { repoLabel.IsChecked = true - labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID)) + labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10)) break } } @@ -983,6 +984,7 @@ func NewIssuePost(ctx *context.Context) { issue := &models.Issue{ RepoID: repo.ID, + Repo: repo, Title: form.Title, PosterID: ctx.User.ID, Poster: ctx.User, @@ -1009,9 +1011,9 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" { - ctx.Redirect(ctx.Repo.RepoLink + "/projects/" + fmt.Sprint(form.ProjectID)) + ctx.Redirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(form.ProjectID, 10)) } else { - ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) } } @@ -1097,13 +1099,16 @@ func ViewIssue(ctx *context.Context) { } return } + if issue.Repo == nil { + issue.Repo = ctx.Repo.Repository + } // Make sure type and URL matches. if ctx.Params(":type") == "issues" && issue.IsPull { - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } else if ctx.Params(":type") == "pulls" && !issue.IsPull { - ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } @@ -1496,7 +1501,7 @@ func ViewIssue(ctx *context.Context) { log.Error("IsProtectedBranch: %v", err) } else if !protected { canDelete = true - ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index) + "/cleanup" + ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup" } } } @@ -1624,7 +1629,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["NumParticipants"] = len(participants) ctx.Data["Issue"] = issue ctx.Data["ReadOnly"] = false - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string) + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) @@ -1773,7 +1778,7 @@ func UpdateIssueContent(ctx *context.Context) { } content, err := markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.FormString("context"), + URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Metas: ctx.Repo.Repository.ComposeMetas(), GitRepo: ctx.Repo.GitRepo, Ctx: ctx, @@ -2205,7 +2210,7 @@ func UpdateCommentContent(ctx *context.Context) { } content, err := markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.FormString("context"), + URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Metas: ctx.Repo.Repository.ComposeMetas(), GitRepo: ctx.Repo.GitRepo, Ctx: ctx, diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index b8efb3b84..0e9405fde 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -94,6 +94,7 @@ func GetActiveStopwatch(c *context.Context) { } c.Data["ActiveStopwatch"] = StopwatchTmplInfo{ + issue.Link(), issue.Repo.FullName(), issue.Index, sw.Seconds() + 1, // ensure time is never zero in ui @@ -102,6 +103,7 @@ func GetActiveStopwatch(c *context.Context) { // StopwatchTmplInfo is a view on a stopwatch specifically for template rendering type StopwatchTmplInfo struct { + IssueLink string RepoSlug string IssueIndex int64 Seconds int64 diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go index 5e24cfa3c..b15c7628d 100644 --- a/routers/web/repo/lfs.go +++ b/routers/web/repo/lfs.go @@ -10,6 +10,7 @@ import ( gotemplate "html/template" "io" "net/http" + "net/url" "path" "strconv" "strings" @@ -285,7 +286,7 @@ func LFSFileGet(ctx *context.Context) { fileSize := meta.Size ctx.Data["FileSize"] = meta.Size - ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct") + ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") switch { case isRepresentableAsText: if st.IsSvgImage() { diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index d5e0a7696..f91c344e9 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -7,6 +7,7 @@ package repo import ( "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -237,7 +238,7 @@ func MigratePost(ctx *context.Context) { err = task.MigrateRepository(ctx.User, ctxUser, opts) if err == nil { - ctx.Redirect(ctxUser.HomeLink() + "/" + opts.RepoName) + ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(opts.RepoName)) return } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 21e1fb2ea..eadc89333 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -6,6 +6,7 @@ package repo import ( "net/http" + "net/url" "time" "code.gitea.io/gitea/models" @@ -244,7 +245,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { } return } - ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action")) + ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.Params(":action"))) } // DeleteMilestone delete a milestone diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 08b285df0..437da14d4 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -7,6 +7,7 @@ package repo import ( "fmt" "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -173,7 +174,7 @@ func ChangeProjectStatus(ctx *context.Context) { } return } - ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action")) + ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + url.QueryEscape(ctx.Params(":action"))) } // DeleteProject delete a project diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0ac05a760..433727821 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -10,8 +10,10 @@ import ( "crypto/subtle" "errors" "fmt" + "html" "net/http" - "path" + "net/url" + "strconv" "strings" "time" @@ -34,7 +36,6 @@ import ( "code.gitea.io/gitea/services/gitdiff" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" - "github.com/unknwon/com" ) const ( @@ -109,8 +110,7 @@ func getForkRepository(ctx *context.Context) *models.Repository { ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID) - ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name - ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID + ctx.Data["ForkRepo"] = forkRepo if err := ctx.User.GetOwnedOrganizations(); err != nil { ctx.ServerError("GetOwnedOrganizations", err) @@ -202,7 +202,7 @@ func ForkPost(ctx *context.Context) { } repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID) if has { - ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name) + ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) return } if !traverseParentRepo.IsFork { @@ -248,7 +248,7 @@ func ForkPost(ctx *context.Context) { } log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name) + ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) } func checkPullInfo(ctx *context.Context) *models.Issue { @@ -682,8 +682,7 @@ func ViewPullFiles(ctx *context.Context) { } } - headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) - setCompareContext(ctx, baseCommit, commit, headTarget) + setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) ctx.Data["RequireHighlightJS"] = true ctx.Data["RequireSimpleMDE"] = true @@ -746,7 +745,7 @@ func UpdatePullRequest(ctx *context.Context) { // ToDo: add check if maintainers are allowed to change branch ... (need migration & co) if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) { ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } @@ -766,7 +765,7 @@ func UpdatePullRequest(ctx *context.Context) { return } ctx.Flash.Error(flashError) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } else if models.IsErrRebaseConflicts(err) { conflictError := err.(models.ErrRebaseConflicts) @@ -780,19 +779,19 @@ func UpdatePullRequest(ctx *context.Context) { return } ctx.Flash.Error(flashError) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } ctx.Flash.Error(err.Error()) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } time.Sleep(1 * time.Second) ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) } // MergePullRequest response for merging pull request @@ -805,11 +804,11 @@ func MergePullRequest(ctx *context.Context) { if issue.IsClosed { if issue.IsPull { ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } ctx.Flash.Error(ctx.Tr("repo.issues.closed_title")) - ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } @@ -822,13 +821,13 @@ func MergePullRequest(ctx *context.Context) { } if !allowedMerge { ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index)) + ctx.Redirect(issue.Link()) return } if pr.HasMerged { ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + ctx.Redirect(issue.Link()) return } @@ -837,11 +836,11 @@ func MergePullRequest(ctx *context.Context) { if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + ctx.Redirect(issue.Link()) return } else if strings.Contains(err.Error(), "Wrong commit ID") { ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + ctx.Redirect(issue.Link()) return } @@ -849,19 +848,19 @@ func MergePullRequest(ctx *context.Context) { return } - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + ctx.Redirect(issue.Link()) return } if !pr.CanAutoMerge() { ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + ctx.Redirect(issue.Link()) return } if pr.IsWorkInProgress() { ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } @@ -875,14 +874,14 @@ func MergePullRequest(ctx *context.Context) { return } else if !isRepoAdmin { ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } } if ctx.HasError() { ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } @@ -914,14 +913,14 @@ func MergePullRequest(ctx *context.Context) { if !noDeps { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } else if models.IsErrMergeConflicts(err) { conflictError := err.(models.ErrMergeConflicts) @@ -935,7 +934,7 @@ func MergePullRequest(ctx *context.Context) { return } ctx.Flash.Error(flashError) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } else if models.IsErrRebaseConflicts(err) { conflictError := err.(models.ErrRebaseConflicts) @@ -949,17 +948,17 @@ func MergePullRequest(ctx *context.Context) { return } ctx.Flash.Error(flashError) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } else if models.IsErrMergeUnrelatedHistories(err) { log.Debug("MergeUnrelatedHistories error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } else if git.IsErrPushOutOfDate(err) { log.Debug("MergePushOutOfDate error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } else if git.IsErrPushRejected(err) { log.Debug("MergePushRejected error: %v", err) @@ -979,7 +978,7 @@ func MergePullRequest(ctx *context.Context) { } ctx.Flash.Error(flashError) } - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) return } ctx.ServerError("Merge", err) @@ -1008,7 +1007,7 @@ func MergePullRequest(ctx *context.Context) { deleteBranch(ctx, pr, headRepo) } - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) + ctx.Redirect(issue.Link()) } func stopTimerIfAvailable(user *models.User, issue *models.Issue) error { @@ -1097,6 +1096,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { pullIssue := &models.Issue{ RepoID: repo.ID, + Repo: repo, Title: form.Title, PosterID: ctx.User.ID, Poster: ctx.User, @@ -1138,7 +1138,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { } ctx.Flash.Error(flashError) } - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index)) + ctx.Redirect(pullIssue.Link()) return } ctx.ServerError("NewPullRequest", err) @@ -1146,7 +1146,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { } log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) - ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index)) + ctx.Redirect(pullIssue.Link()) } // TriggerTask response for a trigger task request @@ -1261,7 +1261,7 @@ func CleanUpPullRequest(ctx *context.Context) { defer func() { ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": pr.BaseRepo.Link() + "/pulls/" + fmt.Sprint(issue.Index), + "redirect": issue.Link(), }) }() @@ -1369,7 +1369,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { err := err.(models.ErrPullRequestAlreadyExists) RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name - errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID) + errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url insidde locale string ctx.Flash.Error(errorMessage) ctx.JSON(http.StatusConflict, map[string]interface{}{ diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 20f6ddd2a..3f12ee72b 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/upload" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" releaseservice "code.gitea.io/gitea/services/release" @@ -350,7 +351,7 @@ func NewReleasePost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + form.TagName) + ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) return } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index c70dec648..46cef7664 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -244,7 +244,7 @@ func CreatePost(ctx *context.Context) { repo, err = repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts) if err == nil { log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name) + ctx.Redirect(repo.Link()) return } } else { @@ -263,7 +263,7 @@ func CreatePost(ctx *context.Context) { }) if err == nil { log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name) + ctx.Redirect(repo.Link()) return } } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index cecd1da07..641052316 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -615,7 +615,7 @@ func SettingsPost(ctx *context.Context) { log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) - ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings") + ctx.Redirect(repo.Link() + "/settings") case "cancel_transfer": if !ctx.Repo.IsOwner() { @@ -627,7 +627,7 @@ func SettingsPost(ctx *context.Context) { if err != nil { if models.IsErrNoPendingTransfer(err) { ctx.Flash.Error("repo.settings.transfer_abort_invalid") - ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings") + ctx.Redirect(repo.Link() + "/settings") } else { ctx.ServerError("GetPendingRepositoryTransfer", err) } @@ -647,7 +647,7 @@ func SettingsPost(ctx *context.Context) { log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name)) - ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings") + ctx.Redirect(repo.Link() + "/settings") case "delete": if !ctx.Repo.IsOwner() { @@ -796,7 +796,7 @@ func Collaboration(ctx *context.Context) { func CollaborationPost(ctx *context.Context) { name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("collaborator"))) if len(name) == 0 || ctx.Repo.Owner.LowerName == name { - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) return } @@ -804,7 +804,7 @@ func CollaborationPost(ctx *context.Context) { if err != nil { if models.IsErrUserNotExist(err) { ctx.Flash.Error(ctx.Tr("form.user_not_exist")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } else { ctx.ServerError("GetUserByName", err) } @@ -813,14 +813,14 @@ func CollaborationPost(ctx *context.Context) { if !u.IsActive { ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) return } // Organization is not allowed to be added as a collaborator. if u.IsOrganization() { ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) return } @@ -840,7 +840,7 @@ func CollaborationPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } // ChangeCollaborationAccessMode response for changing access of a collaboration diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go index 876ff9ba4..32105b1d4 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting_protected_branch.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" pull_service "code.gitea.io/gitea/services/pull" @@ -89,7 +90,7 @@ func ProtectedBranchPost(ctx *context.Context) { log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) default: ctx.NotFound("", nil) } @@ -197,7 +198,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } if f.RequiredApprovals < 0 { ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) - ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch)) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch))) } var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 @@ -274,7 +275,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { return } ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch)) - ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch)) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch))) } else { if protectBranch != nil { if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil { diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go index a180399c9..b4d268759 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/tag.go @@ -58,7 +58,7 @@ func NewProtectedTagPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } // EditProtectedTag render the page to edit a protect tag diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index cecd8437b..12b3aef50 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -232,7 +232,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { } if readmeFile != nil { readmeFile.name = entry.Name() + "/" + readmeFile.name - readmeTreelink = treeLink + "/" + entry.GetSubJumpablePathName() + readmeTreelink = treeLink + "/" + util.PathEscapeSegments(entry.GetSubJumpablePathName()) break } } @@ -301,7 +301,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { fileSize = meta.Size ctx.Data["FileSize"] = meta.Size filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name)) - ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64) + ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.HTMLURL(), url.PathEscape(meta.Oid), url.PathEscape(filenameBase64)) } } } @@ -376,7 +376,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st fileSize := blob.Size() ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileName"] = blob.Name() - ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath + ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) buf := make([]byte, 1024) n, _ := util.ReadAtMost(dataRc, buf) @@ -422,7 +422,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st isTextFile = st.IsText() fileSize = meta.Size - ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath) + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } } } @@ -628,7 +628,7 @@ func checkHomeCodeViewable(ctx *context.Context) { } if firstUnit != nil { - ctx.Redirect(fmt.Sprintf("%s/%s%s", setting.AppSubURL, ctx.Repo.Repository.FullName(), firstUnit.URI)) + ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI)) return } } @@ -684,7 +684,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + ctx.Repo.CommitID + "/" + ctx.Repo.TreePath + ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) // Get current entry user currently looking at. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) @@ -766,7 +766,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri treeLink := branchLink if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + ctx.Repo.TreePath + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } ctx.Data["TreeLink"] = treeLink @@ -815,7 +815,7 @@ func renderCode(ctx *context.Context) { rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + ctx.Repo.TreePath + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } // Get Topics of this repo diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index f47f8d651..4f6660926 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "path" "strings" @@ -414,7 +415,7 @@ func TelegramHooksNewPost(ctx *context.Context) { w := &webhook.Webhook{ RepoID: orCtx.RepoID, - URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID), + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)), ContentType: webhook.ContentTypeJSON, HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, @@ -468,7 +469,7 @@ func MatrixHooksNewPost(ctx *context.Context) { w := &webhook.Webhook{ RepoID: orCtx.RepoID, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID), + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), ContentType: webhook.ContentTypeJSON, HTTPMethod: "PUT", HookEvent: ParseHookEvent(form.WebhookForm), @@ -976,7 +977,7 @@ func TelegramHooksEditPost(ctx *context.Context) { return } w.Meta = string(meta) - w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID) + w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)) w.HookEvent = ParseHookEvent(form.WebhookForm) w.IsActive = form.Active if err := w.UpdateEvent(); err != nil { @@ -1020,7 +1021,7 @@ func MatrixHooksEditPost(ctx *context.Context) { return } w.Meta = string(meta) - w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID) + w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)) w.HookEvent = ParseHookEvent(form.WebhookForm) w.IsActive = form.Active @@ -1162,7 +1163,7 @@ func TestWebhook(ctx *context.Context) { apiCommit := &api.PayloadCommit{ ID: commit.ID.String(), Message: commit.Message(), - URL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(), + URL: ctx.Repo.Repository.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()), Author: &api.PayloadUser{ Name: commit.Author.Name, Email: commit.Author.Email, diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 16927de2e..82f56a8c4 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -180,7 +180,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["Pages"] = pages // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":page")) + pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } @@ -193,7 +193,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { //lookup filename in wiki - get filecontent, gitTree entry , real filename data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { if wikiRepo != nil { @@ -276,7 +276,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":page")) + pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } @@ -291,7 +291,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) //lookup filename in wiki - get filecontent, gitTree entry , real filename data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { if wikiRepo != nil { @@ -352,7 +352,7 @@ func renderEditPage(ctx *context.Context) { }() // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":page")) + pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } @@ -365,7 +365,7 @@ func renderEditPage(ctx *context.Context) { //lookup filename in wiki - get filecontent, gitTree entry , real filename data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { return @@ -378,6 +378,32 @@ func renderEditPage(ctx *context.Context) { ctx.Data["footerContent"] = "" } +// WikiPost renders post of wiki page +func WikiPost(ctx *context.Context) { + switch ctx.FormString("action") { + case "_new": + if !ctx.Repo.CanWrite(unit.TypeWiki) { + ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + return + } + NewWikiPost(ctx) + return + case "_delete": + if !ctx.Repo.CanWrite(unit.TypeWiki) { + ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + return + } + DeleteWikiPagePost(ctx) + return + } + + if !ctx.Repo.CanWrite(unit.TypeWiki) { + ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + return + } + EditWikiPost(ctx) +} + // Wiki renders single wiki page func Wiki(ctx *context.Context) { ctx.Data["PageIsWiki"] = true @@ -389,6 +415,29 @@ func Wiki(ctx *context.Context) { return } + switch ctx.FormString("action") { + case "_pages": + WikiPages(ctx) + return + case "_revision": + WikiRevision(ctx) + return + case "_edit": + if !ctx.Repo.CanWrite(unit.TypeWiki) { + ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + return + } + EditWiki(ctx) + return + case "_new": + if !ctx.Repo.CanWrite(unit.TypeWiki) { + ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + return + } + NewWiki(ctx) + return + } + wikiRepo, entry := renderViewPage(ctx) defer func() { if wikiRepo != nil { @@ -652,7 +701,7 @@ func EditWikiPost(ctx *context.Context) { return } - oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page")) + oldWikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) newWikiName := wiki_service.NormalizeWikiName(form.Title) if len(form.Message) == 0 { @@ -669,7 +718,7 @@ func EditWikiPost(ctx *context.Context) { // DeleteWikiPagePost delete wiki page func DeleteWikiPagePost(ctx *context.Context) { - wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page")) + wikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) if len(wikiName) == 0 { wikiName = "Home" } diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index cf49f19af..87f2779c1 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -76,8 +76,8 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) { func TestWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_pages") - ctx.SetParams(":page", "Home") + ctx := test.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx.SetParams("*", "Home") test.LoadRepo(t, ctx, 1) Wiki(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) @@ -88,7 +88,7 @@ func TestWiki(t *testing.T) { func TestWikiPages(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_pages") + ctx := test.MockContext(t, "user2/repo1/wiki/?action=_pages") test.LoadRepo(t, ctx, 1) WikiPages(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) @@ -98,7 +98,7 @@ func TestWikiPages(t *testing.T) { func TestNewWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_new") + ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) NewWiki(ctx) @@ -113,7 +113,7 @@ func TestNewWikiPost(t *testing.T) { } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_new") + ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.NewWikiForm{ @@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) { func TestNewWikiPost_ReservedName(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_new") + ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.NewWikiForm{ @@ -148,8 +148,8 @@ func TestNewWikiPost_ReservedName(t *testing.T) { func TestEditWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_edit/Home") - ctx.SetParams(":page", "Home") + ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_edit") + ctx.SetParams("*", "Home") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) EditWiki(ctx) @@ -164,8 +164,8 @@ func TestEditWikiPost(t *testing.T) { "New/", } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/_new/Home") - ctx.SetParams(":page", "Home") + ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_new") + ctx.SetParams("*", "Home") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.NewWikiForm{ @@ -186,7 +186,7 @@ func TestEditWikiPost(t *testing.T) { func TestDeleteWikiPagePost(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/Home/delete") + ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_delete") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) DeleteWikiPagePost(ctx) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 1305b0095..b9f5d044f 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -887,5 +887,5 @@ func Email2User(ctx *context.Context) { } return } - ctx.Redirect(setting.AppSubURL + "/user/" + u.Name) + ctx.Redirect(u.HomeLink()) } diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 080ec4b58..08cd1b8b3 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -167,7 +167,7 @@ func NotificationStatusPost(c *context.Context) { } if !c.FormBool("noredirect") { - url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.FormString("page")) + url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(c.FormString("page"))) c.Redirect(url, http.StatusSeeOther) } @@ -189,6 +189,5 @@ func NotificationPurgePost(c *context.Context) { return } - url := fmt.Sprintf("%s/notifications", setting.AppSubURL) - c.Redirect(url, http.StatusSeeOther) + c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) } diff --git a/routers/web/user/oauth.go b/routers/web/user/oauth.go index 642a7f33b..3210d033d 100644 --- a/routers/web/user/oauth.go +++ b/routers/web/user/oauth.go @@ -454,7 +454,7 @@ func AuthorizeOAuth(ctx *context.Context) { ctx.Data["State"] = form.State ctx.Data["Scope"] = form.Scope ctx.Data["Nonce"] = form.Nonce - ctx.Data["ApplicationUserLink"] = "@" + html.EscapeString(user.Name) + "" + ctx.Data["ApplicationUserLink"] = "@" + html.EscapeString(user.Name) + "" ctx.Data["ApplicationRedirectDomainHTML"] = "" + html.EscapeString(form.RedirectURI) + "" // TODO document SESSION <=> FORM err = ctx.Session.Set("client_id", app.ClientID) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index f8fcbf656..17c4783c6 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -364,6 +364,6 @@ func Action(ctx *context.Context) { ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) return } - + // FIXME: We should check this URL and make sure that it's a valid Gitea URL ctx.RedirectToFirst(ctx.FormString("redirect_to"), u.HomeLink()) } diff --git a/routers/web/web.go b/routers/web/web.go index 69f737c3e..a20bf484b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -895,21 +895,23 @@ func RegisterRoutes(m *web.Route) { }, reqRepoProjectsReader, repo.MustEnableProjects) m.Group("/wiki", func() { - m.Get("/", repo.Wiki) - m.Get("/{page}", repo.Wiki) - m.Get("/_pages", repo.WikiPages) - m.Get("/{page}/_revision", repo.WikiRevision) + m.Combo("/"). + Get(repo.Wiki). + Post(context.RepoMustNotBeArchived(), + reqSignIn, + reqRepoWikiWriter, + bindIgnErr(forms.NewWikiForm{}), + repo.WikiPost) + m.Combo("/*"). + Get(repo.Wiki). + Post(context.RepoMustNotBeArchived(), + reqSignIn, + reqRepoWikiWriter, + bindIgnErr(forms.NewWikiForm{}), + repo.WikiPost) m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff) - - m.Group("", func() { - m.Combo("/_new").Get(repo.NewWiki). - Post(bindIgnErr(forms.NewWikiForm{}), repo.NewWikiPost) - m.Combo("/{page}/_edit").Get(repo.EditWiki). - Post(bindIgnErr(forms.NewWikiForm{}), repo.EditWikiPost) - m.Post("/{page}/delete", repo.DeleteWikiPagePost) - }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter) - }, repo.MustEnableWiki, context.RepoRef(), func(ctx *context.Context) { + }, repo.MustEnableWiki, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true }) diff --git a/services/lfs/server.go b/services/lfs/server.go index 5ce2a5498..788765881 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "net/http" + "net/url" "path" "regexp" "strconv" @@ -46,17 +47,17 @@ type Claims struct { // DownloadLink builds a URL to download the object. func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string { - return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/objects", p.Oid) + return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid)) } // UploadLink builds a URL to upload the object. func (rc *requestContext) UploadLink(p lfs_module.Pointer) string { - return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/objects", p.Oid, strconv.FormatInt(p.Size, 10)) + return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10)) } // VerifyLink builds a URL for verifying the object. func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string { - return setting.AppURL + path.Join(rc.User, rc.Repo+".git", "info/lfs/verify") + return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify") } // CheckAcceptMediaType checks if the client accepts the LFS media type. diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 88e407892..a949b073a 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" dingtalk "github.com/lunny/dingtalk_webhook" ) @@ -41,7 +42,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) - return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+refName), nil + return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil } // Delete implements PayloadConvertor Delete method @@ -50,7 +51,7 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) - return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+refName), nil + return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil } // Fork implements PayloadConvertor Fork method @@ -78,7 +79,7 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7]) } if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName + titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName) } title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 3de50a8a2..587d2098e 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) type ( @@ -115,7 +116,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) - return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+refName, greenColor), nil + return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), greenColor), nil } // Delete implements PayloadConvertor Delete method @@ -124,7 +125,7 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) - return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+refName, redColor), nil + return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), redColor), nil } // Fork implements PayloadConvertor Fork method @@ -150,7 +151,7 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { titleLink = p.CompareURL } if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName + titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName) } title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) diff --git a/services/webhook/general.go b/services/webhook/general.go index 777ae086b..32a79c078 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -7,10 +7,12 @@ package webhook import ( "fmt" "html" + "net/url" "strings" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) type linkFormatter = func(string, string) string @@ -22,7 +24,7 @@ func noneLinkFormatter(url string, text string) string { // htmlLinkFormatter creates a HTML link func htmlLinkFormatter(url string, text string) string { - return fmt.Sprintf(`%s`, url, html.EscapeString(text)) + return fmt.Sprintf(`%s`, html.EscapeString(url), html.EscapeString(text)) } func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, withSender bool) (string, string, string, int) { @@ -46,7 +48,7 @@ func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, with case api.HookIssueAssigned: list := make([]string, len(p.Issue.Assignees)) for i, user := range p.Issue.Assignees { - list[i] = linkFormatter(setting.AppURL+user.UserName, user.UserName) + list[i] = linkFormatter(setting.AppURL+url.PathEscape(user.UserName), user.UserName) } text = fmt.Sprintf("[%s] Issue assigned to %s: %s", repoLink, strings.Join(list, ", "), titleLink) color = greenColor @@ -66,7 +68,7 @@ func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, with text = fmt.Sprintf("[%s] Issue milestone cleared: %s", repoLink, titleLink) } if withSender { - text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)) + text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) } var attachmentText string @@ -139,7 +141,7 @@ func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkForm func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName) - refLink := linkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName) + refLink := linkFormatter(p.Repository.HTMLURL+"/src/"+util.PathEscapeSegments(p.Release.TagName), p.Release.TagName) switch p.Action { case api.HookReleasePublished: @@ -153,7 +155,7 @@ func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, w color = redColor } if withSender { - text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)) + text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) } return text, color @@ -189,7 +191,7 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo color = redColor } if withSender { - text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)) + text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) } return text, issueTitle, color diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 08adaef6f..4fd78ff5b 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -10,6 +10,7 @@ import ( "fmt" "html" "net/http" + "net/url" "regexp" "strings" @@ -19,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) const matrixPayloadSizeLimit = 1024 * 64 @@ -94,11 +96,11 @@ func MatrixLinkToRef(repoURL, ref string) string { refName := git.RefEndName(ref) switch { case strings.HasPrefix(ref, git.BranchPrefix): - return MatrixLinkFormatter(repoURL+"/src/branch/"+refName, refName) + return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) case strings.HasPrefix(ref, git.TagPrefix): - return MatrixLinkFormatter(repoURL+"/src/tag/"+refName, refName) + return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) default: - return MatrixLinkFormatter(repoURL+"/src/commit/"+refName, refName) + return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) } } @@ -186,7 +188,7 @@ func (m *MatrixPayloadUnsafe) PullRequest(p *api.PullRequestPayload) (api.Payloa // Review implements PayloadConvertor Review method func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) @@ -281,7 +283,7 @@ func getMatrixHookRequest(w *webhook_model.Webhook, t *webhook_model.HookTask) ( return nil, fmt.Errorf("getMatrixHookRequest: unable to hash payload: %+v", err) } - url := fmt.Sprintf("%s/%s", w.URL, txnID) + url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) req, err := http.NewRequest(w.HTTPMethod, url, strings.NewReader(string(payload))) if err != nil { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 2b88bb23f..ae5af8d9b 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) type ( @@ -79,7 +80,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { p.Sender, title, "", - p.Repo.HTMLURL+"/src/"+refName, + p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), greenColor, &MSTeamsFact{fmt.Sprintf("%s:", p.RefType), refName}, ), nil @@ -96,7 +97,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { p.Sender, title, "", - p.Repo.HTMLURL+"/src/"+refName, + p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), yellowColor, &MSTeamsFact{fmt.Sprintf("%s:", p.RefType), refName}, ), nil @@ -133,7 +134,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { titleLink = p.CompareURL } if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName + titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName) } title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 944099de1..9d57ac432 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -36,7 +36,7 @@ func nameAllowed(name string) error { // NameToSubURL converts a wiki name to its corresponding sub-URL. func NameToSubURL(name string) string { - return url.QueryEscape(strings.ReplaceAll(name, " ", "-")) + return url.PathEscape(strings.ReplaceAll(name, " ", "-")) } // NormalizeWikiName normalizes a wiki name diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index 2d489a495..e73213c1d 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -49,7 +49,7 @@ {{range .Emails}} - {{.Name}} + {{.Name}} {{.FullName}} {{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index e96d9ebb3..4059cb5de 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -45,13 +45,13 @@ {{.ID}} - {{.Owner.Name}} + {{.Owner.Name}} {{if .Owner.Visibility.IsPrivate}} {{svg "octicon-lock"}} {{end}} - {{.Name}} + {{.Name}} {{if .IsArchived}} {{$.i18n.Tr "repo.desc.archived"}} {{end}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index ceab7a9b1..93e6f38c2 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -87,7 +87,7 @@ {{range .Users}} {{.ID}} - {{.Name}} + {{.Name}} {{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .IsAdmin}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index bf1fcd24b..d529e6bfd 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -102,10 +102,10 @@ {{if .IsSigned }} {{ if ne .SignedUser.Theme "gitea" }} - + {{end}} {{else if ne DefaultTheme "gitea"}} - + {{end}} {{template "custom/header" .}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 348e7671a..57ddbf732 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -63,8 +63,7 @@ {{else if .IsSigned}}