summaryrefslogtreecommitdiffstats
path: root/services/wiki/wiki_path.go
blob: 74c706404368e4825e5310a0649ab91c259ad26d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package wiki

import (
	"net/url"
	"path"
	"strings"

	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/modules/git"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/services/convert"
)

// To define the wiki related concepts:
// * Display Segment: the text what user see for a wiki page (aka, the title):
//   - "Home Page"
//   - "100% Free"
//   - "2000-01-02 meeting"
// * Web Path:
//   - "/wiki/Home-Page"
//   - "/wiki/100%25+Free"
//   - "/wiki/2000-01-02+meeting.-"
//   - If a segment has a suffix "DashMarker(.-)", it means that there is no dash-space conversion for this segment.
//   - If a WebPath is a "*.md" pattern, then use the unescaped value directly as GitPath, to make users can access the raw file.
// * Git Path (only space doesn't need to be escaped):
//   - "/.wiki.git/Home-Page.md"
//   - "/.wiki.git/100%25 Free.md"
//   - "/.wiki.git/2000-01-02 meeting.-.md"
// TODO: support subdirectory in the future
//
// Although this package now has the ability to support subdirectory, but the route package doesn't:
// * Double-escaping problem: the URL "/wiki/abc%2Fdef" becomes "/wiki/abc/def" by ctx.Params, which is incorrect
//   * This problem should have been 99% fixed, but it needs more tests.
// * 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.

type WebPath string

var reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}

func validateWebPath(name WebPath) error {
	for _, s := range WebPathSegments(name) {
		if util.SliceContainsString(reservedWikiNames, s) {
			return repo_model.ErrWikiReservedName{Title: s}
		}
	}
	return nil
}

func hasDashMarker(s string) bool {
	return strings.HasSuffix(s, ".-")
}

func removeDashMarker(s string) string {
	return strings.TrimSuffix(s, ".-")
}

func addDashMarker(s string) string {
	return s + ".-"
}

func unescapeSegment(s string) (string, error) {
	if hasDashMarker(s) {
		s = removeDashMarker(s)
	} else {
		s = strings.ReplaceAll(s, "-", " ")
	}
	unescaped, err := url.QueryUnescape(s)
	if err != nil {
		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
	}
	return unescaped, nil
}

func escapeSegToWeb(s string, hadDashMarker bool) string {
	if hadDashMarker || strings.Contains(s, "-") || strings.HasSuffix(s, ".md") {
		s = addDashMarker(s)
	} else {
		s = strings.ReplaceAll(s, " ", "-")
	}
	s = url.QueryEscape(s)
	return s
}

func WebPathSegments(s WebPath) []string {
	a := strings.Split(string(s), "/")
	for i := range a {
		a[i], _ = unescapeSegment(a[i])
	}
	return a
}

func WebPathToGitPath(s WebPath) string {
	if strings.HasSuffix(string(s), ".md") {
		ret, _ := url.PathUnescape(string(s))
		return util.PathJoinRelX(ret)
	}

	a := strings.Split(string(s), "/")
	for i := range a {
		shouldAddDashMarker := hasDashMarker(a[i])
		a[i], _ = unescapeSegment(a[i])
		a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
		a[i] = strings.ReplaceAll(a[i], "%20", " ") // space is safe to be kept in git path
		a[i] = strings.ReplaceAll(a[i], "+", " ")
	}
	return strings.Join(a, "/") + ".md"
}

func GitPathToWebPath(s string) (wp WebPath, err error) {
	if !strings.HasSuffix(s, ".md") {
		return "", repo_model.ErrWikiInvalidFileName{FileName: s}
	}
	s = strings.TrimSuffix(s, ".md")
	a := strings.Split(s, "/")
	for i := range a {
		shouldAddDashMarker := hasDashMarker(a[i])
		if a[i], err = unescapeSegment(a[i]); err != nil {
			return "", err
		}
		a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
	}
	return WebPath(strings.Join(a, "/")), nil
}

func WebPathToUserTitle(s WebPath) (dir, display string) {
	dir = path.Dir(string(s))
	display = path.Base(string(s))
	if strings.HasSuffix(display, ".md") {
		display = strings.TrimSuffix(display, ".md")
		display, _ = url.PathUnescape(display)
	}
	display, _ = unescapeSegment(display)
	return dir, display
}

func WebPathToURLPath(s WebPath) string {
	return string(s)
}

func WebPathFromRequest(s string) WebPath {
	s = util.PathJoinRelX(s)
	// The old wiki code's behavior is always using %2F, instead of subdirectory.
	s = strings.ReplaceAll(s, "/", "%2F")
	return WebPath(s)
}

func UserTitleToWebPath(base, title string) WebPath {
	// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
	// So we do not add the support for writing slashes in title at the moment.
	title = strings.TrimSpace(title)
	title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
	if title == "" || title == "." {
		title = "unnamed"
	}
	return WebPath(title)
}

// ToWikiPageMetaData converts meta information to a WikiPageMetaData
func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
	subURL := string(wikiName)
	_, title := WebPathToUserTitle(wikiName)
	return &api.WikiPageMetaData{
		Title:      title,
		HTMLURL:    util.URLJoin(repo.HTMLURL(), "wiki", subURL),
		SubURL:     subURL,
		LastCommit: convert.ToWikiCommit(lastCommit),
	}
}