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  }