code.gitea.io/gitea@v1.22.3/services/wiki/wiki_path.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package wiki 5 6 import ( 7 "net/url" 8 "path" 9 "strings" 10 11 repo_model "code.gitea.io/gitea/models/repo" 12 "code.gitea.io/gitea/modules/git" 13 api "code.gitea.io/gitea/modules/structs" 14 "code.gitea.io/gitea/modules/util" 15 "code.gitea.io/gitea/services/convert" 16 ) 17 18 // To define the wiki related concepts: 19 // * Display Segment: the text what user see for a wiki page (aka, the title): 20 // - "Home Page" 21 // - "100% Free" 22 // - "2000-01-02 meeting" 23 // * Web Path: 24 // - "/wiki/Home-Page" 25 // - "/wiki/100%25+Free" 26 // - "/wiki/2000-01-02+meeting.-" 27 // - If a segment has a suffix "DashMarker(.-)", it means that there is no dash-space conversion for this segment. 28 // - If a WebPath is a "*.md" pattern, then use the unescaped value directly as GitPath, to make users can access the raw file. 29 // * Git Path (only space doesn't need to be escaped): 30 // - "/.wiki.git/Home-Page.md" 31 // - "/.wiki.git/100%25 Free.md" 32 // - "/.wiki.git/2000-01-02 meeting.-.md" 33 // TODO: support subdirectory in the future 34 // 35 // Although this package now has the ability to support subdirectory, but the route package doesn't: 36 // * Double-escaping problem: the URL "/wiki/abc%2Fdef" becomes "/wiki/abc/def" by ctx.Params, which is incorrect 37 // * This problem should have been 99% fixed, but it needs more tests. 38 // * The old wiki code's behavior is always using %2F, instead of subdirectory, so there are a lot of legacy "%2F" files in user wikis. 39 40 type WebPath string 41 42 var reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"} 43 44 func validateWebPath(name WebPath) error { 45 for _, s := range WebPathSegments(name) { 46 if util.SliceContainsString(reservedWikiNames, s) { 47 return repo_model.ErrWikiReservedName{Title: s} 48 } 49 } 50 return nil 51 } 52 53 func hasDashMarker(s string) bool { 54 return strings.HasSuffix(s, ".-") 55 } 56 57 func removeDashMarker(s string) string { 58 return strings.TrimSuffix(s, ".-") 59 } 60 61 func addDashMarker(s string) string { 62 return s + ".-" 63 } 64 65 func unescapeSegment(s string) (string, error) { 66 if hasDashMarker(s) { 67 s = removeDashMarker(s) 68 } else { 69 s = strings.ReplaceAll(s, "-", " ") 70 } 71 unescaped, err := url.QueryUnescape(s) 72 if err != nil { 73 return s, err // un-escaping failed, but it's still safe to return the original string, because it is only a title for end users 74 } 75 return unescaped, nil 76 } 77 78 func escapeSegToWeb(s string, hadDashMarker bool) string { 79 if hadDashMarker || strings.Contains(s, "-") || strings.HasSuffix(s, ".md") { 80 s = addDashMarker(s) 81 } else { 82 s = strings.ReplaceAll(s, " ", "-") 83 } 84 s = url.QueryEscape(s) 85 return s 86 } 87 88 func WebPathSegments(s WebPath) []string { 89 a := strings.Split(string(s), "/") 90 for i := range a { 91 a[i], _ = unescapeSegment(a[i]) 92 } 93 return a 94 } 95 96 func WebPathToGitPath(s WebPath) string { 97 if strings.HasSuffix(string(s), ".md") { 98 ret, _ := url.PathUnescape(string(s)) 99 return util.PathJoinRelX(ret) 100 } 101 102 a := strings.Split(string(s), "/") 103 for i := range a { 104 shouldAddDashMarker := hasDashMarker(a[i]) 105 a[i], _ = unescapeSegment(a[i]) 106 a[i] = escapeSegToWeb(a[i], shouldAddDashMarker) 107 a[i] = strings.ReplaceAll(a[i], "%20", " ") // space is safe to be kept in git path 108 a[i] = strings.ReplaceAll(a[i], "+", " ") 109 } 110 return strings.Join(a, "/") + ".md" 111 } 112 113 func GitPathToWebPath(s string) (wp WebPath, err error) { 114 if !strings.HasSuffix(s, ".md") { 115 return "", repo_model.ErrWikiInvalidFileName{FileName: s} 116 } 117 s = strings.TrimSuffix(s, ".md") 118 a := strings.Split(s, "/") 119 for i := range a { 120 shouldAddDashMarker := hasDashMarker(a[i]) 121 if a[i], err = unescapeSegment(a[i]); err != nil { 122 return "", err 123 } 124 a[i] = escapeSegToWeb(a[i], shouldAddDashMarker) 125 } 126 return WebPath(strings.Join(a, "/")), nil 127 } 128 129 func WebPathToUserTitle(s WebPath) (dir, display string) { 130 dir = path.Dir(string(s)) 131 display = path.Base(string(s)) 132 if strings.HasSuffix(display, ".md") { 133 display = strings.TrimSuffix(display, ".md") 134 display, _ = url.PathUnescape(display) 135 } 136 display, _ = unescapeSegment(display) 137 return dir, display 138 } 139 140 func WebPathToURLPath(s WebPath) string { 141 return string(s) 142 } 143 144 func WebPathFromRequest(s string) WebPath { 145 s = util.PathJoinRelX(s) 146 // The old wiki code's behavior is always using %2F, instead of subdirectory. 147 s = strings.ReplaceAll(s, "/", "%2F") 148 return WebPath(s) 149 } 150 151 func UserTitleToWebPath(base, title string) WebPath { 152 // TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory. 153 // So we do not add the support for writing slashes in title at the moment. 154 title = strings.TrimSpace(title) 155 title = util.PathJoinRelX(base, escapeSegToWeb(title, false)) 156 if title == "" || title == "." { 157 title = "unnamed" 158 } 159 return WebPath(title) 160 } 161 162 // ToWikiPageMetaData converts meta information to a WikiPageMetaData 163 func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { 164 subURL := string(wikiName) 165 _, title := WebPathToUserTitle(wikiName) 166 return &api.WikiPageMetaData{ 167 Title: title, 168 HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), 169 SubURL: subURL, 170 LastCommit: convert.ToWikiCommit(lastCommit), 171 } 172 }