41c42b968e
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m0s
UserTitleToWebPath now splits on / and sanitizes each segment independently, preserving the directory structure. This allows creating pages like "features/Custom-Fields" as actual nested files.
206 lines
6.3 KiB
Go
206 lines
6.3 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
|
|
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
|
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/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.PathParam, 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 before, ok := strings.CutSuffix(display, ".md"); ok {
|
|
display = before
|
|
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)
|
|
// MokoGitea: support real subdirectories for hierarchical wiki navigation.
|
|
// Slashes are preserved as path separators, not escaped to %2F.
|
|
return WebPath(s)
|
|
}
|
|
|
|
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
|
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
|
|
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
|
|
|
|
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
|
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
|
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/).
|
|
func sanitizeWikiTitle(title string) string {
|
|
title = strings.TrimSpace(title)
|
|
title = strings.ReplaceAll(title, " ", "-")
|
|
// Preserve slashes as directory separators
|
|
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
|
|
title = multiHyphenRe.ReplaceAllString(title, "-")
|
|
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
|
|
title = strings.Trim(title, "-+.")
|
|
return title
|
|
}
|
|
|
|
func UserTitleToWebPath(base, title string) WebPath {
|
|
// MokoGitea: support subdirectories - slashes in title create folder structure.
|
|
// Split on /, sanitize each segment, rejoin.
|
|
parts := strings.Split(title, "/")
|
|
sanitized := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
p = sanitizeWikiTitle(p)
|
|
if p != "" {
|
|
sanitized = append(sanitized, escapeSegToWeb(p, false))
|
|
}
|
|
}
|
|
result := strings.Join(sanitized, "/")
|
|
if base != "" {
|
|
result = util.PathJoinRelX(base, result)
|
|
}
|
|
if result == "" || result == "." {
|
|
result = "unnamed"
|
|
}
|
|
return WebPath(result)
|
|
}
|
|
|
|
// 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: repo.HTMLURL() + "/wiki/" + subURL,
|
|
SubURL: subURL,
|
|
LastCommit: convert.ToWikiCommit(lastCommit),
|
|
}
|
|
}
|