You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

helper.go 29KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // Copyright 2014 The Gogs Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package templates
  5. import (
  6. "bytes"
  7. "context"
  8. "errors"
  9. "fmt"
  10. "html"
  11. "html/template"
  12. "mime"
  13. "net/url"
  14. "path/filepath"
  15. "reflect"
  16. "regexp"
  17. "runtime"
  18. "strconv"
  19. "strings"
  20. texttmpl "text/template"
  21. "time"
  22. "unicode"
  23. activities_model "code.gitea.io/gitea/models/activities"
  24. "code.gitea.io/gitea/models/avatars"
  25. "code.gitea.io/gitea/models/db"
  26. issues_model "code.gitea.io/gitea/models/issues"
  27. "code.gitea.io/gitea/models/organization"
  28. repo_model "code.gitea.io/gitea/models/repo"
  29. system_model "code.gitea.io/gitea/models/system"
  30. user_model "code.gitea.io/gitea/models/user"
  31. "code.gitea.io/gitea/modules/base"
  32. "code.gitea.io/gitea/modules/emoji"
  33. "code.gitea.io/gitea/modules/git"
  34. giturl "code.gitea.io/gitea/modules/git/url"
  35. gitea_html "code.gitea.io/gitea/modules/html"
  36. "code.gitea.io/gitea/modules/json"
  37. "code.gitea.io/gitea/modules/log"
  38. "code.gitea.io/gitea/modules/markup"
  39. "code.gitea.io/gitea/modules/markup/markdown"
  40. "code.gitea.io/gitea/modules/repository"
  41. "code.gitea.io/gitea/modules/setting"
  42. "code.gitea.io/gitea/modules/svg"
  43. "code.gitea.io/gitea/modules/timeutil"
  44. "code.gitea.io/gitea/modules/util"
  45. "code.gitea.io/gitea/services/gitdiff"
  46. "github.com/editorconfig/editorconfig-core-go/v2"
  47. )
  48. // Used from static.go && dynamic.go
  49. var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
  50. // NewFuncMap returns functions for injecting to templates
  51. func NewFuncMap() []template.FuncMap {
  52. return []template.FuncMap{map[string]interface{}{
  53. "GoVer": func() string {
  54. return util.ToTitleCase(runtime.Version())
  55. },
  56. "UseHTTPS": func() bool {
  57. return strings.HasPrefix(setting.AppURL, "https")
  58. },
  59. "AppName": func() string {
  60. return setting.AppName
  61. },
  62. "AppSubUrl": func() string {
  63. return setting.AppSubURL
  64. },
  65. "AssetUrlPrefix": func() string {
  66. return setting.StaticURLPrefix + "/assets"
  67. },
  68. "AppUrl": func() string {
  69. return setting.AppURL
  70. },
  71. "AppVer": func() string {
  72. return setting.AppVer
  73. },
  74. "AppBuiltWith": func() string {
  75. return setting.AppBuiltWith
  76. },
  77. "AppDomain": func() string {
  78. return setting.Domain
  79. },
  80. "AssetVersion": func() string {
  81. return setting.AssetVersion
  82. },
  83. "DisableGravatar": func() bool {
  84. return system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
  85. },
  86. "DefaultShowFullName": func() bool {
  87. return setting.UI.DefaultShowFullName
  88. },
  89. "ShowFooterTemplateLoadTime": func() bool {
  90. return setting.ShowFooterTemplateLoadTime
  91. },
  92. "LoadTimes": func(startTime time.Time) string {
  93. return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
  94. },
  95. "AllowedReactions": func() []string {
  96. return setting.UI.Reactions
  97. },
  98. "CustomEmojis": func() map[string]string {
  99. return setting.UI.CustomEmojisMap
  100. },
  101. "Safe": Safe,
  102. "SafeJS": SafeJS,
  103. "JSEscape": JSEscape,
  104. "Str2html": Str2html,
  105. "TimeSince": timeutil.TimeSince,
  106. "TimeSinceUnix": timeutil.TimeSinceUnix,
  107. "FileSize": base.FileSize,
  108. "PrettyNumber": base.PrettyNumber,
  109. "JsPrettyNumber": JsPrettyNumber,
  110. "Subtract": base.Subtract,
  111. "EntryIcon": base.EntryIcon,
  112. "MigrationIcon": MigrationIcon,
  113. "Add": func(a ...int) int {
  114. sum := 0
  115. for _, val := range a {
  116. sum += val
  117. }
  118. return sum
  119. },
  120. "Mul": func(a ...int) int {
  121. sum := 1
  122. for _, val := range a {
  123. sum *= val
  124. }
  125. return sum
  126. },
  127. "ActionIcon": ActionIcon,
  128. "DateFmtLong": func(t time.Time) string {
  129. return t.Format(time.RFC1123Z)
  130. },
  131. "DateFmtShort": func(t time.Time) string {
  132. return t.Format("Jan 02, 2006")
  133. },
  134. "CountFmt": base.FormatNumberSI,
  135. "SubStr": func(str string, start, length int) string {
  136. if len(str) == 0 {
  137. return ""
  138. }
  139. end := start + length
  140. if length == -1 {
  141. end = len(str)
  142. }
  143. if len(str) < end {
  144. return str
  145. }
  146. return str[start:end]
  147. },
  148. "EllipsisString": base.EllipsisString,
  149. "DiffTypeToStr": DiffTypeToStr,
  150. "DiffLineTypeToStr": DiffLineTypeToStr,
  151. "ShortSha": base.ShortSha,
  152. "ActionContent2Commits": ActionContent2Commits,
  153. "PathEscape": url.PathEscape,
  154. "PathEscapeSegments": util.PathEscapeSegments,
  155. "URLJoin": util.URLJoin,
  156. "RenderCommitMessage": RenderCommitMessage,
  157. "RenderCommitMessageLink": RenderCommitMessageLink,
  158. "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
  159. "RenderCommitBody": RenderCommitBody,
  160. "RenderCodeBlock": RenderCodeBlock,
  161. "RenderIssueTitle": RenderIssueTitle,
  162. "RenderEmoji": RenderEmoji,
  163. "RenderEmojiPlain": emoji.ReplaceAliases,
  164. "ReactionToEmoji": ReactionToEmoji,
  165. "RenderNote": RenderNote,
  166. "RenderMarkdownToHtml": func(input string) template.HTML {
  167. output, err := markdown.RenderString(&markup.RenderContext{
  168. URLPrefix: setting.AppSubURL,
  169. }, input)
  170. if err != nil {
  171. log.Error("RenderString: %v", err)
  172. }
  173. return template.HTML(output)
  174. },
  175. "IsMultilineCommitMessage": IsMultilineCommitMessage,
  176. "ThemeColorMetaTag": func() string {
  177. return setting.UI.ThemeColorMetaTag
  178. },
  179. "MetaAuthor": func() string {
  180. return setting.UI.Meta.Author
  181. },
  182. "MetaDescription": func() string {
  183. return setting.UI.Meta.Description
  184. },
  185. "MetaKeywords": func() string {
  186. return setting.UI.Meta.Keywords
  187. },
  188. "UseServiceWorker": func() bool {
  189. return setting.UI.UseServiceWorker
  190. },
  191. "EnableTimetracking": func() bool {
  192. return setting.Service.EnableTimetracking
  193. },
  194. "FilenameIsImage": func(filename string) bool {
  195. mimeType := mime.TypeByExtension(filepath.Ext(filename))
  196. return strings.HasPrefix(mimeType, "image/")
  197. },
  198. "TabSizeClass": func(ec interface{}, filename string) string {
  199. var (
  200. value *editorconfig.Editorconfig
  201. ok bool
  202. )
  203. if ec != nil {
  204. if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
  205. return "tab-size-8"
  206. }
  207. def, err := value.GetDefinitionForFilename(filename)
  208. if err != nil {
  209. log.Error("tab size class: getting definition for filename: %v", err)
  210. return "tab-size-8"
  211. }
  212. if def.TabWidth > 0 {
  213. return fmt.Sprintf("tab-size-%d", def.TabWidth)
  214. }
  215. }
  216. return "tab-size-8"
  217. },
  218. "SubJumpablePath": func(str string) []string {
  219. var path []string
  220. index := strings.LastIndex(str, "/")
  221. if index != -1 && index != len(str) {
  222. path = append(path, str[0:index+1], str[index+1:])
  223. } else {
  224. path = append(path, str)
  225. }
  226. return path
  227. },
  228. "DiffStatsWidth": func(adds, dels int) string {
  229. return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100)
  230. },
  231. "Json": func(in interface{}) string {
  232. out, err := json.Marshal(in)
  233. if err != nil {
  234. return ""
  235. }
  236. return string(out)
  237. },
  238. "JsonPrettyPrint": func(in string) string {
  239. var out bytes.Buffer
  240. err := json.Indent(&out, []byte(in), "", " ")
  241. if err != nil {
  242. return ""
  243. }
  244. return out.String()
  245. },
  246. "DisableGitHooks": func() bool {
  247. return setting.DisableGitHooks
  248. },
  249. "DisableWebhooks": func() bool {
  250. return setting.DisableWebhooks
  251. },
  252. "DisableImportLocal": func() bool {
  253. return !setting.ImportLocalPaths
  254. },
  255. "Dict": func(values ...interface{}) (map[string]interface{}, error) {
  256. if len(values)%2 != 0 {
  257. return nil, errors.New("invalid dict call")
  258. }
  259. dict := make(map[string]interface{}, len(values)/2)
  260. for i := 0; i < len(values); i += 2 {
  261. key, ok := values[i].(string)
  262. if !ok {
  263. return nil, errors.New("dict keys must be strings")
  264. }
  265. dict[key] = values[i+1]
  266. }
  267. return dict, nil
  268. },
  269. "Printf": fmt.Sprintf,
  270. "Escape": Escape,
  271. "Sec2Time": util.SecToTime,
  272. "ParseDeadline": func(deadline string) []string {
  273. return strings.Split(deadline, "|")
  274. },
  275. "DefaultTheme": func() string {
  276. return setting.UI.DefaultTheme
  277. },
  278. // pass key-value pairs to a partial template which receives them as a dict
  279. "dict": func(values ...interface{}) (map[string]interface{}, error) {
  280. if len(values) == 0 {
  281. return nil, errors.New("invalid dict call")
  282. }
  283. dict := make(map[string]interface{})
  284. return util.MergeInto(dict, values...)
  285. },
  286. /* like dict but merge key-value pairs into the first dict and return it */
  287. "mergeinto": func(root map[string]interface{}, values ...interface{}) (map[string]interface{}, error) {
  288. if len(values) == 0 {
  289. return nil, errors.New("invalid mergeinto call")
  290. }
  291. dict := make(map[string]interface{})
  292. for key, value := range root {
  293. dict[key] = value
  294. }
  295. return util.MergeInto(dict, values...)
  296. },
  297. "percentage": func(n int, values ...int) float32 {
  298. sum := 0
  299. for i := 0; i < len(values); i++ {
  300. sum += values[i]
  301. }
  302. return float32(n) * 100 / float32(sum)
  303. },
  304. "CommentMustAsDiff": gitdiff.CommentMustAsDiff,
  305. "MirrorRemoteAddress": mirrorRemoteAddress,
  306. "NotificationSettings": func() map[string]interface{} {
  307. return map[string]interface{}{
  308. "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
  309. "TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
  310. "MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
  311. "EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
  312. }
  313. },
  314. "containGeneric": func(arr, v interface{}) bool {
  315. arrV := reflect.ValueOf(arr)
  316. if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
  317. return strings.Contains(arr.(string), v.(string))
  318. }
  319. if arrV.Kind() == reflect.Slice {
  320. for i := 0; i < arrV.Len(); i++ {
  321. iV := arrV.Index(i)
  322. if !iV.CanInterface() {
  323. continue
  324. }
  325. if iV.Interface() == v {
  326. return true
  327. }
  328. }
  329. }
  330. return false
  331. },
  332. "contain": func(s []int64, id int64) bool {
  333. for i := 0; i < len(s); i++ {
  334. if s[i] == id {
  335. return true
  336. }
  337. }
  338. return false
  339. },
  340. "svg": svg.RenderHTML,
  341. "avatar": Avatar,
  342. "avatarHTML": AvatarHTML,
  343. "avatarByAction": AvatarByAction,
  344. "avatarByEmail": AvatarByEmail,
  345. "repoAvatar": RepoAvatar,
  346. "SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
  347. // if needed
  348. if len(normSort) == 0 || len(urlSort) == 0 {
  349. return ""
  350. }
  351. if len(urlSort) == 0 && isDefault {
  352. // if sort is sorted as default add arrow tho this table header
  353. if isDefault {
  354. return svg.RenderHTML("octicon-triangle-down", 16)
  355. }
  356. } else {
  357. // if sort arg is in url test if it correlates with column header sort arguments
  358. // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
  359. if urlSort == normSort {
  360. // the table is sorted with this header normal
  361. return svg.RenderHTML("octicon-triangle-up", 16)
  362. } else if urlSort == revSort {
  363. // the table is sorted with this header reverse
  364. return svg.RenderHTML("octicon-triangle-down", 16)
  365. }
  366. }
  367. // the table is NOT sorted with this header
  368. return ""
  369. },
  370. "RenderLabels": func(labels []*issues_model.Label, repoLink string) template.HTML {
  371. htmlCode := `<span class="labels-list">`
  372. for _, label := range labels {
  373. // Protect against nil value in labels - shouldn't happen but would cause a panic if so
  374. if label == nil {
  375. continue
  376. }
  377. htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d' class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</a> ",
  378. repoLink, label.ID, label.ForegroundColor(), label.Color, html.EscapeString(label.Description), RenderEmoji(label.Name))
  379. }
  380. htmlCode += "</span>"
  381. return template.HTML(htmlCode)
  382. },
  383. "MermaidMaxSourceCharacters": func() int {
  384. return setting.MermaidMaxSourceCharacters
  385. },
  386. "Join": strings.Join,
  387. "QueryEscape": url.QueryEscape,
  388. "DotEscape": DotEscape,
  389. "Iterate": func(arg interface{}) (items []uint64) {
  390. count := uint64(0)
  391. switch val := arg.(type) {
  392. case uint64:
  393. count = val
  394. case *uint64:
  395. count = *val
  396. case int64:
  397. if val < 0 {
  398. val = 0
  399. }
  400. count = uint64(val)
  401. case *int64:
  402. if *val < 0 {
  403. *val = 0
  404. }
  405. count = uint64(*val)
  406. case int:
  407. if val < 0 {
  408. val = 0
  409. }
  410. count = uint64(val)
  411. case *int:
  412. if *val < 0 {
  413. *val = 0
  414. }
  415. count = uint64(*val)
  416. case uint:
  417. count = uint64(val)
  418. case *uint:
  419. count = uint64(*val)
  420. case int32:
  421. if val < 0 {
  422. val = 0
  423. }
  424. count = uint64(val)
  425. case *int32:
  426. if *val < 0 {
  427. *val = 0
  428. }
  429. count = uint64(*val)
  430. case uint32:
  431. count = uint64(val)
  432. case *uint32:
  433. count = uint64(*val)
  434. case string:
  435. cnt, _ := strconv.ParseInt(val, 10, 64)
  436. if cnt < 0 {
  437. cnt = 0
  438. }
  439. count = uint64(cnt)
  440. }
  441. if count <= 0 {
  442. return items
  443. }
  444. for i := uint64(0); i < count; i++ {
  445. items = append(items, i)
  446. }
  447. return items
  448. },
  449. "HasPrefix": strings.HasPrefix,
  450. "CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
  451. var curBranch string
  452. if repo.ID != baseRepo.ID {
  453. curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
  454. }
  455. curBranch += util.PathEscapeSegments(branchName)
  456. return fmt.Sprintf("%s/compare/%s...%s",
  457. baseRepo.Link(),
  458. util.PathEscapeSegments(baseRepo.DefaultBranch),
  459. curBranch,
  460. )
  461. },
  462. "RefShortName": func(ref string) string {
  463. return git.RefName(ref).ShortName()
  464. },
  465. }}
  466. }
  467. // NewTextFuncMap returns functions for injecting to text templates
  468. // It's a subset of those used for HTML and other templates
  469. func NewTextFuncMap() []texttmpl.FuncMap {
  470. return []texttmpl.FuncMap{map[string]interface{}{
  471. "GoVer": func() string {
  472. return util.ToTitleCase(runtime.Version())
  473. },
  474. "AppName": func() string {
  475. return setting.AppName
  476. },
  477. "AppSubUrl": func() string {
  478. return setting.AppSubURL
  479. },
  480. "AppUrl": func() string {
  481. return setting.AppURL
  482. },
  483. "AppVer": func() string {
  484. return setting.AppVer
  485. },
  486. "AppBuiltWith": func() string {
  487. return setting.AppBuiltWith
  488. },
  489. "AppDomain": func() string {
  490. return setting.Domain
  491. },
  492. "TimeSince": timeutil.TimeSince,
  493. "TimeSinceUnix": timeutil.TimeSinceUnix,
  494. "DateFmtLong": func(t time.Time) string {
  495. return t.Format(time.RFC1123Z)
  496. },
  497. "DateFmtShort": func(t time.Time) string {
  498. return t.Format("Jan 02, 2006")
  499. },
  500. "SubStr": func(str string, start, length int) string {
  501. if len(str) == 0 {
  502. return ""
  503. }
  504. end := start + length
  505. if length == -1 {
  506. end = len(str)
  507. }
  508. if len(str) < end {
  509. return str
  510. }
  511. return str[start:end]
  512. },
  513. "EllipsisString": base.EllipsisString,
  514. "URLJoin": util.URLJoin,
  515. "Dict": func(values ...interface{}) (map[string]interface{}, error) {
  516. if len(values)%2 != 0 {
  517. return nil, errors.New("invalid dict call")
  518. }
  519. dict := make(map[string]interface{}, len(values)/2)
  520. for i := 0; i < len(values); i += 2 {
  521. key, ok := values[i].(string)
  522. if !ok {
  523. return nil, errors.New("dict keys must be strings")
  524. }
  525. dict[key] = values[i+1]
  526. }
  527. return dict, nil
  528. },
  529. "Printf": fmt.Sprintf,
  530. "Escape": Escape,
  531. "Sec2Time": util.SecToTime,
  532. "ParseDeadline": func(deadline string) []string {
  533. return strings.Split(deadline, "|")
  534. },
  535. "dict": func(values ...interface{}) (map[string]interface{}, error) {
  536. if len(values) == 0 {
  537. return nil, errors.New("invalid dict call")
  538. }
  539. dict := make(map[string]interface{})
  540. for i := 0; i < len(values); i++ {
  541. switch key := values[i].(type) {
  542. case string:
  543. i++
  544. if i == len(values) {
  545. return nil, errors.New("specify the key for non array values")
  546. }
  547. dict[key] = values[i]
  548. case map[string]interface{}:
  549. m := values[i].(map[string]interface{})
  550. for i, v := range m {
  551. dict[i] = v
  552. }
  553. default:
  554. return nil, errors.New("dict values must be maps")
  555. }
  556. }
  557. return dict, nil
  558. },
  559. "percentage": func(n int, values ...int) float32 {
  560. sum := 0
  561. for i := 0; i < len(values); i++ {
  562. sum += values[i]
  563. }
  564. return float32(n) * 100 / float32(sum)
  565. },
  566. "Add": func(a ...int) int {
  567. sum := 0
  568. for _, val := range a {
  569. sum += val
  570. }
  571. return sum
  572. },
  573. "Mul": func(a ...int) int {
  574. sum := 1
  575. for _, val := range a {
  576. sum *= val
  577. }
  578. return sum
  579. },
  580. "QueryEscape": url.QueryEscape,
  581. }}
  582. }
  583. // AvatarHTML creates the HTML for an avatar
  584. func AvatarHTML(src string, size int, class, name string) template.HTML {
  585. sizeStr := fmt.Sprintf(`%d`, size)
  586. if name == "" {
  587. name = "avatar"
  588. }
  589. return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
  590. }
  591. // Avatar renders user avatars. args: user, size (int), class (string)
  592. func Avatar(item interface{}, others ...interface{}) template.HTML {
  593. size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
  594. switch t := item.(type) {
  595. case *user_model.User:
  596. src := t.AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
  597. if src != "" {
  598. return AvatarHTML(src, size, class, t.DisplayName())
  599. }
  600. case *repo_model.Collaborator:
  601. src := t.AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
  602. if src != "" {
  603. return AvatarHTML(src, size, class, t.DisplayName())
  604. }
  605. case *organization.Organization:
  606. src := t.AsUser().AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
  607. if src != "" {
  608. return AvatarHTML(src, size, class, t.AsUser().DisplayName())
  609. }
  610. }
  611. return template.HTML("")
  612. }
  613. // AvatarByAction renders user avatars from action. args: action, size (int), class (string)
  614. func AvatarByAction(action *activities_model.Action, others ...interface{}) template.HTML {
  615. action.LoadActUser(db.DefaultContext)
  616. return Avatar(action.ActUser, others...)
  617. }
  618. // RepoAvatar renders repo avatars. args: repo, size(int), class (string)
  619. func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
  620. size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
  621. src := repo.RelAvatarLink()
  622. if src != "" {
  623. return AvatarHTML(src, size, class, repo.FullName())
  624. }
  625. return template.HTML("")
  626. }
  627. // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
  628. func AvatarByEmail(email, name string, others ...interface{}) template.HTML {
  629. size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
  630. src := avatars.GenerateEmailAvatarFastLink(email, size*setting.Avatar.RenderedSizeFactor)
  631. if src != "" {
  632. return AvatarHTML(src, size, class, name)
  633. }
  634. return template.HTML("")
  635. }
  636. // Safe render raw as HTML
  637. func Safe(raw string) template.HTML {
  638. return template.HTML(raw)
  639. }
  640. // SafeJS renders raw as JS
  641. func SafeJS(raw string) template.JS {
  642. return template.JS(raw)
  643. }
  644. // Str2html render Markdown text to HTML
  645. func Str2html(raw string) template.HTML {
  646. return template.HTML(markup.Sanitize(raw))
  647. }
  648. // Escape escapes a HTML string
  649. func Escape(raw string) string {
  650. return html.EscapeString(raw)
  651. }
  652. // JSEscape escapes a JS string
  653. func JSEscape(raw string) string {
  654. return template.JSEscapeString(raw)
  655. }
  656. // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
  657. func DotEscape(raw string) string {
  658. return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
  659. }
  660. // RenderCommitMessage renders commit message with XSS-safe and special links.
  661. func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
  662. return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
  663. }
  664. // RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
  665. // default url, handling for special links.
  666. func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
  667. cleanMsg := template.HTMLEscapeString(msg)
  668. // we can safely assume that it will not return any error, since there
  669. // shouldn't be any special HTML.
  670. fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
  671. Ctx: ctx,
  672. URLPrefix: urlPrefix,
  673. DefaultLink: urlDefault,
  674. Metas: metas,
  675. }, cleanMsg)
  676. if err != nil {
  677. log.Error("RenderCommitMessage: %v", err)
  678. return ""
  679. }
  680. msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
  681. if len(msgLines) == 0 {
  682. return template.HTML("")
  683. }
  684. return template.HTML(msgLines[0])
  685. }
  686. // RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
  687. // the provided default url, handling for special links without email to links.
  688. func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
  689. msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
  690. lineEnd := strings.IndexByte(msgLine, '\n')
  691. if lineEnd > 0 {
  692. msgLine = msgLine[:lineEnd]
  693. }
  694. msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
  695. if len(msgLine) == 0 {
  696. return template.HTML("")
  697. }
  698. // we can safely assume that it will not return any error, since there
  699. // shouldn't be any special HTML.
  700. renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
  701. Ctx: ctx,
  702. URLPrefix: urlPrefix,
  703. DefaultLink: urlDefault,
  704. Metas: metas,
  705. }, template.HTMLEscapeString(msgLine))
  706. if err != nil {
  707. log.Error("RenderCommitMessageSubject: %v", err)
  708. return template.HTML("")
  709. }
  710. return template.HTML(renderedMessage)
  711. }
  712. // RenderCommitBody extracts the body of a commit message without its title.
  713. func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
  714. msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
  715. lineEnd := strings.IndexByte(msgLine, '\n')
  716. if lineEnd > 0 {
  717. msgLine = msgLine[lineEnd+1:]
  718. } else {
  719. return template.HTML("")
  720. }
  721. msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
  722. if len(msgLine) == 0 {
  723. return template.HTML("")
  724. }
  725. renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
  726. Ctx: ctx,
  727. URLPrefix: urlPrefix,
  728. Metas: metas,
  729. }, template.HTMLEscapeString(msgLine))
  730. if err != nil {
  731. log.Error("RenderCommitMessage: %v", err)
  732. return ""
  733. }
  734. return template.HTML(renderedMessage)
  735. }
  736. // Match text that is between back ticks.
  737. var codeMatcher = regexp.MustCompile("`([^`]+)`")
  738. // RenderCodeBlock renders "`…`" as highlighted "<code>" block.
  739. // Intended for issue and PR titles, these containers should have styles for "<code>" elements
  740. func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
  741. htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
  742. return template.HTML(htmlWithCodeTags)
  743. }
  744. // RenderIssueTitle renders issue/pull title with defined post processors
  745. func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
  746. renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
  747. Ctx: ctx,
  748. URLPrefix: urlPrefix,
  749. Metas: metas,
  750. }, template.HTMLEscapeString(text))
  751. if err != nil {
  752. log.Error("RenderIssueTitle: %v", err)
  753. return template.HTML("")
  754. }
  755. return template.HTML(renderedText)
  756. }
  757. // RenderEmoji renders html text with emoji post processors
  758. func RenderEmoji(text string) template.HTML {
  759. renderedText, err := markup.RenderEmoji(template.HTMLEscapeString(text))
  760. if err != nil {
  761. log.Error("RenderEmoji: %v", err)
  762. return template.HTML("")
  763. }
  764. return template.HTML(renderedText)
  765. }
  766. // ReactionToEmoji renders emoji for use in reactions
  767. func ReactionToEmoji(reaction string) template.HTML {
  768. val := emoji.FromCode(reaction)
  769. if val != nil {
  770. return template.HTML(val.Emoji)
  771. }
  772. val = emoji.FromAlias(reaction)
  773. if val != nil {
  774. return template.HTML(val.Emoji)
  775. }
  776. return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
  777. }
  778. // RenderNote renders the contents of a git-notes file as a commit message.
  779. func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
  780. cleanMsg := template.HTMLEscapeString(msg)
  781. fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
  782. Ctx: ctx,
  783. URLPrefix: urlPrefix,
  784. Metas: metas,
  785. }, cleanMsg)
  786. if err != nil {
  787. log.Error("RenderNote: %v", err)
  788. return ""
  789. }
  790. return template.HTML(fullMessage)
  791. }
  792. // IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
  793. func IsMultilineCommitMessage(msg string) bool {
  794. return strings.Count(strings.TrimSpace(msg), "\n") >= 1
  795. }
  796. // Actioner describes an action
  797. type Actioner interface {
  798. GetOpType() activities_model.ActionType
  799. GetActUserName() string
  800. GetRepoUserName() string
  801. GetRepoName() string
  802. GetRepoPath() string
  803. GetRepoLink() string
  804. GetBranch() string
  805. GetContent() string
  806. GetCreate() time.Time
  807. GetIssueInfos() []string
  808. }
  809. // ActionIcon accepts an action operation type and returns an icon class name.
  810. func ActionIcon(opType activities_model.ActionType) string {
  811. switch opType {
  812. case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
  813. return "repo"
  814. case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
  815. return "git-commit"
  816. case activities_model.ActionCreateIssue:
  817. return "issue-opened"
  818. case activities_model.ActionCreatePullRequest:
  819. return "git-pull-request"
  820. case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
  821. return "comment-discussion"
  822. case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
  823. return "git-merge"
  824. case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
  825. return "issue-closed"
  826. case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
  827. return "issue-reopened"
  828. case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
  829. return "mirror"
  830. case activities_model.ActionApprovePullRequest:
  831. return "check"
  832. case activities_model.ActionRejectPullRequest:
  833. return "diff"
  834. case activities_model.ActionPublishRelease:
  835. return "tag"
  836. case activities_model.ActionPullReviewDismissed:
  837. return "x"
  838. default:
  839. return "question"
  840. }
  841. }
  842. // ActionContent2Commits converts action content to push commits
  843. func ActionContent2Commits(act Actioner) *repository.PushCommits {
  844. push := repository.NewPushCommits()
  845. if act == nil || act.GetContent() == "" {
  846. return push
  847. }
  848. if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
  849. log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
  850. }
  851. if push.Len == 0 {
  852. push.Len = len(push.Commits)
  853. }
  854. return push
  855. }
  856. // DiffTypeToStr returns diff type name
  857. func DiffTypeToStr(diffType int) string {
  858. diffTypes := map[int]string{
  859. 1: "add", 2: "modify", 3: "del", 4: "rename", 5: "copy",
  860. }
  861. return diffTypes[diffType]
  862. }
  863. // DiffLineTypeToStr returns diff line type name
  864. func DiffLineTypeToStr(diffType int) string {
  865. switch diffType {
  866. case 2:
  867. return "add"
  868. case 3:
  869. return "del"
  870. case 4:
  871. return "tag"
  872. }
  873. return "same"
  874. }
  875. // MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
  876. func MigrationIcon(hostname string) string {
  877. switch hostname {
  878. case "github.com":
  879. return "octicon-mark-github"
  880. default:
  881. return "gitea-git"
  882. }
  883. }
  884. func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
  885. // Split template into subject and body
  886. var subjectContent []byte
  887. bodyContent := content
  888. loc := mailSubjectSplit.FindIndex(content)
  889. if loc != nil {
  890. subjectContent = content[0:loc[0]]
  891. bodyContent = content[loc[1]:]
  892. }
  893. if _, err := stpl.New(name).
  894. Parse(string(subjectContent)); err != nil {
  895. log.Warn("Failed to parse template [%s/subject]: %v", name, err)
  896. }
  897. if _, err := btpl.New(name).
  898. Parse(string(bodyContent)); err != nil {
  899. log.Warn("Failed to parse template [%s/body]: %v", name, err)
  900. }
  901. }
  902. type remoteAddress struct {
  903. Address string
  904. Username string
  905. Password string
  906. }
  907. func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
  908. a := remoteAddress{}
  909. remoteURL := m.OriginalURL
  910. if ignoreOriginalURL || remoteURL == "" {
  911. var err error
  912. remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
  913. if err != nil {
  914. log.Error("GetRemoteURL %v", err)
  915. return a
  916. }
  917. }
  918. u, err := giturl.Parse(remoteURL)
  919. if err != nil {
  920. log.Error("giturl.Parse %v", err)
  921. return a
  922. }
  923. if u.Scheme != "ssh" && u.Scheme != "file" {
  924. if u.User != nil {
  925. a.Username = u.User.Username()
  926. a.Password, _ = u.User.Password()
  927. }
  928. u.User = nil
  929. }
  930. a.Address = u.String()
  931. return a
  932. }
  933. // JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent
  934. // JS will replace the number with locale-specific separators, based on the user's selected language
  935. func JsPrettyNumber(i interface{}) template.HTML {
  936. num := util.NumberIntoInt64(i)
  937. return template.HTML(`<span class="js-pretty-number" data-value="` + strconv.FormatInt(num, 10) + `">` + base.PrettyNumber(num) + `</span>`)
  938. }