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.

tool.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. // Copyright 2014 The Gogs 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 base
  5. import (
  6. "crypto/md5"
  7. "crypto/rand"
  8. "crypto/sha1"
  9. "encoding/base64"
  10. "encoding/hex"
  11. "fmt"
  12. "html/template"
  13. "io"
  14. "math"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "path"
  19. "path/filepath"
  20. "runtime"
  21. "strconv"
  22. "strings"
  23. "time"
  24. "unicode"
  25. "unicode/utf8"
  26. "code.gitea.io/gitea/modules/git"
  27. "code.gitea.io/gitea/modules/log"
  28. "code.gitea.io/gitea/modules/setting"
  29. "code.gitea.io/gitea/modules/util"
  30. "github.com/Unknwon/com"
  31. "github.com/Unknwon/i18n"
  32. "github.com/gogits/chardet"
  33. )
  34. // EncodeMD5 encodes string to md5 hex value.
  35. func EncodeMD5(str string) string {
  36. m := md5.New()
  37. m.Write([]byte(str))
  38. return hex.EncodeToString(m.Sum(nil))
  39. }
  40. // EncodeSha1 string to sha1 hex value.
  41. func EncodeSha1(str string) string {
  42. h := sha1.New()
  43. h.Write([]byte(str))
  44. return hex.EncodeToString(h.Sum(nil))
  45. }
  46. // ShortSha is basically just truncating.
  47. // It is DEPRECATED and will be removed in the future.
  48. func ShortSha(sha1 string) string {
  49. return TruncateString(sha1, 10)
  50. }
  51. // DetectEncoding detect the encoding of content
  52. func DetectEncoding(content []byte) (string, error) {
  53. if utf8.Valid(content) {
  54. log.Debug("Detected encoding: utf-8 (fast)")
  55. return "UTF-8", nil
  56. }
  57. textDetector := chardet.NewTextDetector()
  58. var detectContent []byte
  59. if len(content) < 1024 {
  60. // Check if original content is valid
  61. if _, err := textDetector.DetectBest(content); err != nil {
  62. return "", err
  63. }
  64. times := 1024 / len(content)
  65. detectContent = make([]byte, 0, times*len(content))
  66. for i := 0; i < times; i++ {
  67. detectContent = append(detectContent, content...)
  68. }
  69. } else {
  70. detectContent = content
  71. }
  72. result, err := textDetector.DetectBest(detectContent)
  73. if err != nil {
  74. return "", err
  75. }
  76. if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
  77. log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
  78. return setting.Repository.AnsiCharset, err
  79. }
  80. log.Debug("Detected encoding: %s", result.Charset)
  81. return result.Charset, err
  82. }
  83. // BasicAuthDecode decode basic auth string
  84. func BasicAuthDecode(encoded string) (string, string, error) {
  85. s, err := base64.StdEncoding.DecodeString(encoded)
  86. if err != nil {
  87. return "", "", err
  88. }
  89. auth := strings.SplitN(string(s), ":", 2)
  90. return auth[0], auth[1], nil
  91. }
  92. // BasicAuthEncode encode basic auth string
  93. func BasicAuthEncode(username, password string) string {
  94. return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
  95. }
  96. // GetRandomBytesAsBase64 generates a random base64 string from n bytes
  97. func GetRandomBytesAsBase64(n int) string {
  98. bytes := make([]byte, 32)
  99. _, err := io.ReadFull(rand.Reader, bytes)
  100. if err != nil {
  101. log.Fatal("Error reading random bytes: %v", err)
  102. }
  103. return base64.RawURLEncoding.EncodeToString(bytes)
  104. }
  105. // VerifyTimeLimitCode verify time limit code
  106. func VerifyTimeLimitCode(data string, minutes int, code string) bool {
  107. if len(code) <= 18 {
  108. return false
  109. }
  110. // split code
  111. start := code[:12]
  112. lives := code[12:18]
  113. if d, err := com.StrTo(lives).Int(); err == nil {
  114. minutes = d
  115. }
  116. // right active code
  117. retCode := CreateTimeLimitCode(data, minutes, start)
  118. if retCode == code && minutes > 0 {
  119. // check time is expired or not
  120. before, _ := time.ParseInLocation("200601021504", start, time.Local)
  121. now := time.Now()
  122. if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
  123. return true
  124. }
  125. }
  126. return false
  127. }
  128. // TimeLimitCodeLength default value for time limit code
  129. const TimeLimitCodeLength = 12 + 6 + 40
  130. // CreateTimeLimitCode create a time limit code
  131. // code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
  132. func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string {
  133. format := "200601021504"
  134. var start, end time.Time
  135. var startStr, endStr string
  136. if startInf == nil {
  137. // Use now time create code
  138. start = time.Now()
  139. startStr = start.Format(format)
  140. } else {
  141. // use start string create code
  142. startStr = startInf.(string)
  143. start, _ = time.ParseInLocation(format, startStr, time.Local)
  144. startStr = start.Format(format)
  145. }
  146. end = start.Add(time.Minute * time.Duration(minutes))
  147. endStr = end.Format(format)
  148. // create sha1 encode string
  149. sh := sha1.New()
  150. sh.Write([]byte(data + setting.SecretKey + startStr + endStr + com.ToStr(minutes)))
  151. encoded := hex.EncodeToString(sh.Sum(nil))
  152. code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
  153. return code
  154. }
  155. // HashEmail hashes email address to MD5 string.
  156. // https://en.gravatar.com/site/implement/hash/
  157. func HashEmail(email string) string {
  158. return EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
  159. }
  160. // DefaultAvatarLink the default avatar link
  161. func DefaultAvatarLink() string {
  162. return setting.AppSubURL + "/img/avatar_default.png"
  163. }
  164. // DefaultAvatarSize is a sentinel value for the default avatar size, as
  165. // determined by the avatar-hosting service.
  166. const DefaultAvatarSize = -1
  167. // libravatarURL returns the URL for the given email. This function should only
  168. // be called if a federated avatar service is enabled.
  169. func libravatarURL(email string) (*url.URL, error) {
  170. urlStr, err := setting.LibravatarService.FromEmail(email)
  171. if err != nil {
  172. log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
  173. return nil, err
  174. }
  175. u, err := url.Parse(urlStr)
  176. if err != nil {
  177. log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
  178. return nil, err
  179. }
  180. return u, nil
  181. }
  182. // SizedAvatarLink returns a sized link to the avatar for the given email
  183. // address.
  184. func SizedAvatarLink(email string, size int) string {
  185. var avatarURL *url.URL
  186. if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
  187. var err error
  188. avatarURL, err = libravatarURL(email)
  189. if err != nil {
  190. return DefaultAvatarLink()
  191. }
  192. } else if !setting.DisableGravatar {
  193. // copy GravatarSourceURL, because we will modify its Path.
  194. copyOfGravatarSourceURL := *setting.GravatarSourceURL
  195. avatarURL = &copyOfGravatarSourceURL
  196. avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
  197. } else {
  198. return DefaultAvatarLink()
  199. }
  200. vals := avatarURL.Query()
  201. vals.Set("d", "identicon")
  202. if size != DefaultAvatarSize {
  203. vals.Set("s", strconv.Itoa(size))
  204. }
  205. avatarURL.RawQuery = vals.Encode()
  206. return avatarURL.String()
  207. }
  208. // AvatarLink returns relative avatar link to the site domain by given email,
  209. // which includes app sub-url as prefix. However, it is possible
  210. // to return full URL if user enables Gravatar-like service.
  211. func AvatarLink(email string) string {
  212. return SizedAvatarLink(email, DefaultAvatarSize)
  213. }
  214. // Seconds-based time units
  215. const (
  216. Minute = 60
  217. Hour = 60 * Minute
  218. Day = 24 * Hour
  219. Week = 7 * Day
  220. Month = 30 * Day
  221. Year = 12 * Month
  222. )
  223. func computeTimeDiff(diff int64, lang string) (int64, string) {
  224. diffStr := ""
  225. switch {
  226. case diff <= 0:
  227. diff = 0
  228. diffStr = i18n.Tr(lang, "tool.now")
  229. case diff < 2:
  230. diff = 0
  231. diffStr = i18n.Tr(lang, "tool.1s")
  232. case diff < 1*Minute:
  233. diffStr = i18n.Tr(lang, "tool.seconds", diff)
  234. diff = 0
  235. case diff < 2*Minute:
  236. diff -= 1 * Minute
  237. diffStr = i18n.Tr(lang, "tool.1m")
  238. case diff < 1*Hour:
  239. diffStr = i18n.Tr(lang, "tool.minutes", diff/Minute)
  240. diff -= diff / Minute * Minute
  241. case diff < 2*Hour:
  242. diff -= 1 * Hour
  243. diffStr = i18n.Tr(lang, "tool.1h")
  244. case diff < 1*Day:
  245. diffStr = i18n.Tr(lang, "tool.hours", diff/Hour)
  246. diff -= diff / Hour * Hour
  247. case diff < 2*Day:
  248. diff -= 1 * Day
  249. diffStr = i18n.Tr(lang, "tool.1d")
  250. case diff < 1*Week:
  251. diffStr = i18n.Tr(lang, "tool.days", diff/Day)
  252. diff -= diff / Day * Day
  253. case diff < 2*Week:
  254. diff -= 1 * Week
  255. diffStr = i18n.Tr(lang, "tool.1w")
  256. case diff < 1*Month:
  257. diffStr = i18n.Tr(lang, "tool.weeks", diff/Week)
  258. diff -= diff / Week * Week
  259. case diff < 2*Month:
  260. diff -= 1 * Month
  261. diffStr = i18n.Tr(lang, "tool.1mon")
  262. case diff < 1*Year:
  263. diffStr = i18n.Tr(lang, "tool.months", diff/Month)
  264. diff -= diff / Month * Month
  265. case diff < 2*Year:
  266. diff -= 1 * Year
  267. diffStr = i18n.Tr(lang, "tool.1y")
  268. default:
  269. diffStr = i18n.Tr(lang, "tool.years", diff/Year)
  270. diff -= (diff / Year) * Year
  271. }
  272. return diff, diffStr
  273. }
  274. // MinutesToFriendly returns a user friendly string with number of minutes
  275. // converted to hours and minutes.
  276. func MinutesToFriendly(minutes int, lang string) string {
  277. duration := time.Duration(minutes) * time.Minute
  278. return TimeSincePro(time.Now().Add(-duration), lang)
  279. }
  280. // TimeSincePro calculates the time interval and generate full user-friendly string.
  281. func TimeSincePro(then time.Time, lang string) string {
  282. return timeSincePro(then, time.Now(), lang)
  283. }
  284. func timeSincePro(then, now time.Time, lang string) string {
  285. diff := now.Unix() - then.Unix()
  286. if then.After(now) {
  287. return i18n.Tr(lang, "tool.future")
  288. }
  289. if diff == 0 {
  290. return i18n.Tr(lang, "tool.now")
  291. }
  292. var timeStr, diffStr string
  293. for {
  294. if diff == 0 {
  295. break
  296. }
  297. diff, diffStr = computeTimeDiff(diff, lang)
  298. timeStr += ", " + diffStr
  299. }
  300. return strings.TrimPrefix(timeStr, ", ")
  301. }
  302. func timeSince(then, now time.Time, lang string) string {
  303. return timeSinceUnix(then.Unix(), now.Unix(), lang)
  304. }
  305. func timeSinceUnix(then, now int64, lang string) string {
  306. lbl := "tool.ago"
  307. diff := now - then
  308. if then > now {
  309. lbl = "tool.from_now"
  310. diff = then - now
  311. }
  312. if diff <= 0 {
  313. return i18n.Tr(lang, "tool.now")
  314. }
  315. _, diffStr := computeTimeDiff(diff, lang)
  316. return i18n.Tr(lang, lbl, diffStr)
  317. }
  318. // RawTimeSince retrieves i18n key of time since t
  319. func RawTimeSince(t time.Time, lang string) string {
  320. return timeSince(t, time.Now(), lang)
  321. }
  322. // TimeSince calculates the time interval and generate user-friendly string.
  323. func TimeSince(then time.Time, lang string) template.HTML {
  324. return htmlTimeSince(then, time.Now(), lang)
  325. }
  326. func htmlTimeSince(then, now time.Time, lang string) template.HTML {
  327. return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`,
  328. then.Format(setting.TimeFormat),
  329. timeSince(then, now, lang)))
  330. }
  331. // TimeSinceUnix calculates the time interval and generate user-friendly string.
  332. func TimeSinceUnix(then util.TimeStamp, lang string) template.HTML {
  333. return htmlTimeSinceUnix(then, util.TimeStamp(time.Now().Unix()), lang)
  334. }
  335. func htmlTimeSinceUnix(then, now util.TimeStamp, lang string) template.HTML {
  336. return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`,
  337. then.Format(setting.TimeFormat),
  338. timeSinceUnix(int64(then), int64(now), lang)))
  339. }
  340. // Storage space size types
  341. const (
  342. Byte = 1
  343. KByte = Byte * 1024
  344. MByte = KByte * 1024
  345. GByte = MByte * 1024
  346. TByte = GByte * 1024
  347. PByte = TByte * 1024
  348. EByte = PByte * 1024
  349. )
  350. var bytesSizeTable = map[string]uint64{
  351. "b": Byte,
  352. "kb": KByte,
  353. "mb": MByte,
  354. "gb": GByte,
  355. "tb": TByte,
  356. "pb": PByte,
  357. "eb": EByte,
  358. }
  359. func logn(n, b float64) float64 {
  360. return math.Log(n) / math.Log(b)
  361. }
  362. func humanateBytes(s uint64, base float64, sizes []string) string {
  363. if s < 10 {
  364. return fmt.Sprintf("%dB", s)
  365. }
  366. e := math.Floor(logn(float64(s), base))
  367. suffix := sizes[int(e)]
  368. val := float64(s) / math.Pow(base, math.Floor(e))
  369. f := "%.0f"
  370. if val < 10 {
  371. f = "%.1f"
  372. }
  373. return fmt.Sprintf(f+"%s", val, suffix)
  374. }
  375. // FileSize calculates the file size and generate user-friendly string.
  376. func FileSize(s int64) string {
  377. sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
  378. return humanateBytes(uint64(s), 1024, sizes)
  379. }
  380. // Subtract deals with subtraction of all types of number.
  381. func Subtract(left interface{}, right interface{}) interface{} {
  382. var rleft, rright int64
  383. var fleft, fright float64
  384. var isInt = true
  385. switch left := left.(type) {
  386. case int:
  387. rleft = int64(left)
  388. case int8:
  389. rleft = int64(left)
  390. case int16:
  391. rleft = int64(left)
  392. case int32:
  393. rleft = int64(left)
  394. case int64:
  395. rleft = left
  396. case float32:
  397. fleft = float64(left)
  398. isInt = false
  399. case float64:
  400. fleft = left
  401. isInt = false
  402. }
  403. switch right := right.(type) {
  404. case int:
  405. rright = int64(right)
  406. case int8:
  407. rright = int64(right)
  408. case int16:
  409. rright = int64(right)
  410. case int32:
  411. rright = int64(right)
  412. case int64:
  413. rright = right
  414. case float32:
  415. fright = float64(right)
  416. isInt = false
  417. case float64:
  418. fright = right
  419. isInt = false
  420. }
  421. if isInt {
  422. return rleft - rright
  423. }
  424. return fleft + float64(rleft) - (fright + float64(rright))
  425. }
  426. // EllipsisString returns a truncated short string,
  427. // it appends '...' in the end of the length of string is too large.
  428. func EllipsisString(str string, length int) string {
  429. if length <= 3 {
  430. return "..."
  431. }
  432. if len(str) <= length {
  433. return str
  434. }
  435. return str[:length-3] + "..."
  436. }
  437. // TruncateString returns a truncated string with given limit,
  438. // it returns input string if length is not reached limit.
  439. func TruncateString(str string, limit int) string {
  440. if len(str) < limit {
  441. return str
  442. }
  443. return str[:limit]
  444. }
  445. // StringsToInt64s converts a slice of string to a slice of int64.
  446. func StringsToInt64s(strs []string) ([]int64, error) {
  447. ints := make([]int64, len(strs))
  448. for i := range strs {
  449. n, err := com.StrTo(strs[i]).Int64()
  450. if err != nil {
  451. return ints, err
  452. }
  453. ints[i] = n
  454. }
  455. return ints, nil
  456. }
  457. // Int64sToStrings converts a slice of int64 to a slice of string.
  458. func Int64sToStrings(ints []int64) []string {
  459. strs := make([]string, len(ints))
  460. for i := range ints {
  461. strs[i] = strconv.FormatInt(ints[i], 10)
  462. }
  463. return strs
  464. }
  465. // Int64sToMap converts a slice of int64 to a int64 map.
  466. func Int64sToMap(ints []int64) map[int64]bool {
  467. m := make(map[int64]bool)
  468. for _, i := range ints {
  469. m[i] = true
  470. }
  471. return m
  472. }
  473. // Int64sContains returns if a int64 in a slice of int64
  474. func Int64sContains(intsSlice []int64, a int64) bool {
  475. for _, c := range intsSlice {
  476. if c == a {
  477. return true
  478. }
  479. }
  480. return false
  481. }
  482. // IsLetter reports whether the rune is a letter (category L).
  483. // https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257
  484. func IsLetter(ch rune) bool {
  485. return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
  486. }
  487. // IsTextFile returns true if file content format is plain text or empty.
  488. func IsTextFile(data []byte) bool {
  489. if len(data) == 0 {
  490. return true
  491. }
  492. return strings.Index(http.DetectContentType(data), "text/") != -1
  493. }
  494. // IsImageFile detects if data is an image format
  495. func IsImageFile(data []byte) bool {
  496. return strings.Index(http.DetectContentType(data), "image/") != -1
  497. }
  498. // IsPDFFile detects if data is a pdf format
  499. func IsPDFFile(data []byte) bool {
  500. return strings.Index(http.DetectContentType(data), "application/pdf") != -1
  501. }
  502. // IsVideoFile detects if data is an video format
  503. func IsVideoFile(data []byte) bool {
  504. return strings.Index(http.DetectContentType(data), "video/") != -1
  505. }
  506. // IsAudioFile detects if data is an video format
  507. func IsAudioFile(data []byte) bool {
  508. return strings.Index(http.DetectContentType(data), "audio/") != -1
  509. }
  510. // EntryIcon returns the octicon class for displaying files/directories
  511. func EntryIcon(entry *git.TreeEntry) string {
  512. switch {
  513. case entry.IsLink():
  514. te, err := entry.FollowLink()
  515. if err != nil {
  516. log.Debug(err.Error())
  517. return "file-symlink-file"
  518. }
  519. if te.IsDir() {
  520. return "file-symlink-directory"
  521. }
  522. return "file-symlink-file"
  523. case entry.IsDir():
  524. return "file-directory"
  525. case entry.IsSubModule():
  526. return "file-submodule"
  527. }
  528. return "file-text"
  529. }
  530. // SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
  531. func SetupGiteaRoot() string {
  532. giteaRoot := os.Getenv("GITEA_ROOT")
  533. if giteaRoot == "" {
  534. _, filename, _, _ := runtime.Caller(0)
  535. giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
  536. wd, err := os.Getwd()
  537. if err != nil {
  538. rel, err := filepath.Rel(giteaRoot, wd)
  539. if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
  540. giteaRoot = wd
  541. }
  542. }
  543. if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
  544. giteaRoot = ""
  545. } else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
  546. giteaRoot = ""
  547. }
  548. }
  549. return giteaRoot
  550. }