本站源代码
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

323 linhas
10KB

  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package references
  5. import (
  6. "net/url"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "code.gitea.io/gitea/modules/markup/mdstripper"
  12. "code.gitea.io/gitea/modules/setting"
  13. )
  14. var (
  15. // validNamePattern performs only the most basic validation for user or repository names
  16. // Repository name should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.
  17. validNamePattern = regexp.MustCompile(`^[a-z0-9_.-]+$`)
  18. // NOTE: All below regex matching do not perform any extra validation.
  19. // Thus a link is produced even if the linked entity does not exist.
  20. // While fast, this is also incorrect and lead to false positives.
  21. // TODO: fix invalid linking issue
  22. // mentionPattern matches all mentions in the form of "@user"
  23. mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`)
  24. // issueNumericPattern matches string that references to a numeric issue, e.g. #1287
  25. issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`)
  26. // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
  27. issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
  28. // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
  29. // e.g. gogits/gogs#12345
  30. crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
  31. // Same as GitHub. See
  32. // https://help.github.com/articles/closing-issues-via-commit-messages
  33. issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
  34. issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
  35. issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
  36. giteaHostInit sync.Once
  37. giteaHost string
  38. )
  39. // XRefAction represents the kind of effect a cross reference has once is resolved
  40. type XRefAction int64
  41. const (
  42. // XRefActionNone means the cross-reference is simply a comment
  43. XRefActionNone XRefAction = iota // 0
  44. // XRefActionCloses means the cross-reference should close an issue if it is resolved
  45. XRefActionCloses // 1
  46. // XRefActionReopens means the cross-reference should reopen an issue if it is resolved
  47. XRefActionReopens // 2
  48. // XRefActionNeutered means the cross-reference will no longer affect the source
  49. XRefActionNeutered // 3
  50. )
  51. // IssueReference contains an unverified cross-reference to a local issue or pull request
  52. type IssueReference struct {
  53. Index int64
  54. Owner string
  55. Name string
  56. Action XRefAction
  57. }
  58. // RenderizableReference contains an unverified cross-reference to with rendering information
  59. type RenderizableReference struct {
  60. Issue string
  61. Owner string
  62. Name string
  63. RefLocation *RefSpan
  64. Action XRefAction
  65. ActionLocation *RefSpan
  66. }
  67. type rawReference struct {
  68. index int64
  69. owner string
  70. name string
  71. action XRefAction
  72. issue string
  73. refLocation *RefSpan
  74. actionLocation *RefSpan
  75. }
  76. func rawToIssueReferenceList(reflist []*rawReference) []IssueReference {
  77. refarr := make([]IssueReference, len(reflist))
  78. for i, r := range reflist {
  79. refarr[i] = IssueReference{
  80. Index: r.index,
  81. Owner: r.owner,
  82. Name: r.name,
  83. Action: r.action,
  84. }
  85. }
  86. return refarr
  87. }
  88. // RefSpan is the position where the reference was found within the parsed text
  89. type RefSpan struct {
  90. Start int
  91. End int
  92. }
  93. func makeKeywordsPat(keywords []string) *regexp.Regexp {
  94. return regexp.MustCompile(`(?i)(?:\s|^|\(|\[)(` + strings.Join(keywords, `|`) + `):? $`)
  95. }
  96. func init() {
  97. issueCloseKeywordsPat = makeKeywordsPat(issueCloseKeywords)
  98. issueReopenKeywordsPat = makeKeywordsPat(issueReopenKeywords)
  99. }
  100. // getGiteaHostName returns a normalized string with the local host name, with no scheme or port information
  101. func getGiteaHostName() string {
  102. giteaHostInit.Do(func() {
  103. if uapp, err := url.Parse(setting.AppURL); err == nil {
  104. giteaHost = strings.ToLower(uapp.Host)
  105. } else {
  106. giteaHost = ""
  107. }
  108. })
  109. return giteaHost
  110. }
  111. // FindAllMentionsMarkdown matches mention patterns in given content and
  112. // returns a list of found unvalidated user names **not including** the @ prefix.
  113. func FindAllMentionsMarkdown(content string) []string {
  114. bcontent, _ := mdstripper.StripMarkdownBytes([]byte(content))
  115. locations := FindAllMentionsBytes(bcontent)
  116. mentions := make([]string, len(locations))
  117. for i, val := range locations {
  118. mentions[i] = string(bcontent[val.Start+1 : val.End])
  119. }
  120. return mentions
  121. }
  122. // FindAllMentionsBytes matches mention patterns in given content
  123. // and returns a list of locations for the unvalidated user names, including the @ prefix.
  124. func FindAllMentionsBytes(content []byte) []RefSpan {
  125. mentions := mentionPattern.FindAllSubmatchIndex(content, -1)
  126. ret := make([]RefSpan, len(mentions))
  127. for i, val := range mentions {
  128. ret[i] = RefSpan{Start: val[2], End: val[3]}
  129. }
  130. return ret
  131. }
  132. // FindFirstMentionBytes matches the first mention in then given content
  133. // and returns the location of the unvalidated user name, including the @ prefix.
  134. func FindFirstMentionBytes(content []byte) (bool, RefSpan) {
  135. mention := mentionPattern.FindSubmatchIndex(content)
  136. if mention == nil {
  137. return false, RefSpan{}
  138. }
  139. return true, RefSpan{Start: mention[2], End: mention[3]}
  140. }
  141. // FindAllIssueReferencesMarkdown strips content from markdown markup
  142. // and returns a list of unvalidated references found in it.
  143. func FindAllIssueReferencesMarkdown(content string) []IssueReference {
  144. return rawToIssueReferenceList(findAllIssueReferencesMarkdown(content))
  145. }
  146. func findAllIssueReferencesMarkdown(content string) []*rawReference {
  147. bcontent, links := mdstripper.StripMarkdownBytes([]byte(content))
  148. return findAllIssueReferencesBytes(bcontent, links)
  149. }
  150. // FindAllIssueReferences returns a list of unvalidated references found in a string.
  151. func FindAllIssueReferences(content string) []IssueReference {
  152. return rawToIssueReferenceList(findAllIssueReferencesBytes([]byte(content), []string{}))
  153. }
  154. // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
  155. func FindRenderizableReferenceNumeric(content string) (bool, *RenderizableReference) {
  156. match := issueNumericPattern.FindStringSubmatchIndex(content)
  157. if match == nil {
  158. if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil {
  159. return false, nil
  160. }
  161. }
  162. r := getCrossReference([]byte(content), match[2], match[3], false)
  163. if r == nil {
  164. return false, nil
  165. }
  166. return true, &RenderizableReference{
  167. Issue: r.issue,
  168. Owner: r.owner,
  169. Name: r.name,
  170. RefLocation: r.refLocation,
  171. Action: r.action,
  172. ActionLocation: r.actionLocation,
  173. }
  174. }
  175. // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
  176. func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
  177. match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
  178. if match == nil {
  179. return false, nil
  180. }
  181. action, location := findActionKeywords([]byte(content), match[2])
  182. return true, &RenderizableReference{
  183. Issue: string(content[match[2]:match[3]]),
  184. RefLocation: &RefSpan{Start: match[2], End: match[3]},
  185. Action: action,
  186. ActionLocation: location,
  187. }
  188. }
  189. // FindAllIssueReferencesBytes returns a list of unvalidated references found in a byte slice.
  190. func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference {
  191. ret := make([]*rawReference, 0, 10)
  192. matches := issueNumericPattern.FindAllSubmatchIndex(content, -1)
  193. for _, match := range matches {
  194. if ref := getCrossReference(content, match[2], match[3], false); ref != nil {
  195. ret = append(ret, ref)
  196. }
  197. }
  198. matches = crossReferenceIssueNumericPattern.FindAllSubmatchIndex(content, -1)
  199. for _, match := range matches {
  200. if ref := getCrossReference(content, match[2], match[3], false); ref != nil {
  201. ret = append(ret, ref)
  202. }
  203. }
  204. localhost := getGiteaHostName()
  205. for _, link := range links {
  206. if u, err := url.Parse(link); err == nil {
  207. // Note: we're not attempting to match the URL scheme (http/https)
  208. host := strings.ToLower(u.Host)
  209. if host != "" && host != localhost {
  210. continue
  211. }
  212. parts := strings.Split(u.EscapedPath(), "/")
  213. // /user/repo/issues/3
  214. if len(parts) != 5 || parts[0] != "" {
  215. continue
  216. }
  217. if parts[3] != "issues" && parts[3] != "pulls" {
  218. continue
  219. }
  220. // Note: closing/reopening keywords not supported with URLs
  221. bytes := []byte(parts[1] + "/" + parts[2] + "#" + parts[4])
  222. if ref := getCrossReference(bytes, 0, len(bytes), true); ref != nil {
  223. ref.refLocation = nil
  224. ret = append(ret, ref)
  225. }
  226. }
  227. }
  228. return ret
  229. }
  230. func getCrossReference(content []byte, start, end int, fromLink bool) *rawReference {
  231. refid := string(content[start:end])
  232. parts := strings.Split(refid, "#")
  233. if len(parts) != 2 {
  234. return nil
  235. }
  236. repo, issue := parts[0], parts[1]
  237. index, err := strconv.ParseInt(issue, 10, 64)
  238. if err != nil {
  239. return nil
  240. }
  241. if repo == "" {
  242. if fromLink {
  243. // Markdown links must specify owner/repo
  244. return nil
  245. }
  246. action, location := findActionKeywords(content, start)
  247. return &rawReference{
  248. index: index,
  249. action: action,
  250. issue: issue,
  251. refLocation: &RefSpan{Start: start, End: end},
  252. actionLocation: location,
  253. }
  254. }
  255. parts = strings.Split(strings.ToLower(repo), "/")
  256. if len(parts) != 2 {
  257. return nil
  258. }
  259. owner, name := parts[0], parts[1]
  260. if !validNamePattern.MatchString(owner) || !validNamePattern.MatchString(name) {
  261. return nil
  262. }
  263. action, location := findActionKeywords(content, start)
  264. return &rawReference{
  265. index: index,
  266. owner: owner,
  267. name: name,
  268. action: action,
  269. issue: issue,
  270. refLocation: &RefSpan{Start: start, End: end},
  271. actionLocation: location,
  272. }
  273. }
  274. func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
  275. m := issueCloseKeywordsPat.FindSubmatchIndex(content[:start])
  276. if m != nil {
  277. return XRefActionCloses, &RefSpan{Start: m[2], End: m[3]}
  278. }
  279. m = issueReopenKeywordsPat.FindSubmatchIndex(content[:start])
  280. if m != nil {
  281. return XRefActionReopens, &RefSpan{Start: m[2], End: m[3]}
  282. }
  283. return XRefActionNone, nil
  284. }
上海开阖软件有限公司 沪ICP备12045867号-1