0 package gmi
1
2 import (
3 "bytes"
4 "errors"
5 "fmt"
6 "gemigit/access"
7 "gemigit/config"
8 "gemigit/db"
9 "gemigit/repo"
10 io "gemigit/util"
11 "log"
12 "strconv"
13 "strings"
14 "text/template"
15
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/object"
18 "github.com/go-git/go-git/v5/plumbing/transport"
19 "github.com/pitr/gig"
20 )
21
22 func execT(c gig.Context, template string, data interface{}) error {
23 t := templates.Lookup(template)
24 var b bytes.Buffer
25 err := t.Execute(&b, data)
26 if err != nil {
27 log.Println(err.Error())
28 return c.NoContent(gig.StatusTemporaryFailure, err.Error())
29 }
30 return c.Gemini(b.String())
31 }
32
33 func execTemplate(template string, data interface{}) (string, error) {
34 t := templates.Lookup(template)
35 var b bytes.Buffer
36 err := t.Execute(&b, data)
37 if err != nil {
38 log.Println(err.Error())
39 return "", err
40 }
41 return strings.TrimRight(b.String(), "\n"), nil
42 }
43
44 const (
45 pageLog = iota
46 pageFiles
47 pageRefs
48 pageLicense
49 pageReadme
50 )
51
52 var templates *template.Template
53
54 func LoadTemplate(dir string) error {
55 var err error
56
57 dirlen := len(dir)
58 if dirlen > 1 && dir[dirlen - 1] == '/' {
59 dir = dir[:dirlen - 1]
60 }
61
62 templates = template.New("gmi")
63 template.Must(templates.Funcs(template.FuncMap {
64 "AccessFirst": accessFirstOption,
65 "AccessSecond": accessSecondOption,
66 "AccessPrivilege": privilegeToString,
67 }).ParseFiles(
68 dir + "/index.gmi",
69 dir + "/account.gmi",
70 dir + "/repo.gmi",
71 dir + "/repo_log.gmi",
72 dir + "/repo_files.gmi",
73 dir + "/repo_refs.gmi",
74 dir + "/repo_license.gmi",
75 dir + "/repo_readme.gmi",
76 dir + "/repo_access.gmi",
77 dir + "/register_success.gmi",
78 dir + "/public_repo.gmi",
79 dir + "/group_list.gmi",
80 dir + "/group.gmi",
81 dir + "/public_list.gmi",
82 dir + "/public_user.gmi",
83 dir + "/otp.gmi",
84 dir + "/token.gmi",
85 dir + "/token_new.gmi",
86 ))
87 if err != nil {
88 return err
89 }
90 log.Println("Templates loaded")
91 return nil
92 }
93
94 func showRepoFile(user string, reponame string, file string) (string, error) {
95 out, err := repo.GetFile(reponame, user, file)
96 if err != nil {
97 return "", err
98 }
99 reader, err := out.Reader()
100 if err != nil {
101 return "", err
102 }
103 buf, err := io.ReadAll(reader)
104 if err != nil {
105 return "", err
106 }
107 return string(buf), nil
108 }
109
110 func ShowIndex(c gig.Context) (error) {
111 _, connected := db.GetUser(c.CertHash())
112 data := struct {
113 Title string
114 Registration bool
115 Connected bool
116 Public bool
117 }{
118 Title: config.Cfg.Title,
119 Registration: config.Cfg.Users.Registration,
120 Connected: connected,
121 Public: connected || config.Cfg.Git.Public,
122 }
123 return execT(c, "index.gmi", data)
124 }
125
126 func ShowAccount(c gig.Context) (error) {
127 user, exist := db.GetUser(c.CertHash())
128 if !exist {
129 return c.NoContent(gig.StatusBadRequest, "Invalid username")
130 }
131 repoNames := []string{}
132 repos, err := user.GetRepos(false)
133 if err != nil {
134 repoNames = []string{"Failed to load repositories"}
135 log.Println(err)
136 } else {
137 for _, repo := range repos {
138 repoNames = append(repoNames, repo.Name)
139 }
140 }
141 accessRepos, err := user.HasReadAccessTo()
142 sessions, err := user.GetSessionsCount()
143 if err != nil {
144 log.Println(err)
145 return c.NoContent(gig.StatusBadRequest, "Unexpected error")
146 }
147 if sessions == 1 {
148 sessions = 0
149 }
150 data := struct {
151 Username string
152 Description string
153 Repositories []string
154 RepositoriesAccess []db.Repo
155 Sessions int
156 }{
157 Username: user.Name,
158 Description: user.Description,
159 Repositories: repoNames,
160 RepositoriesAccess: accessRepos,
161 Sessions: sessions,
162 }
163 return execT(c, "account.gmi", data)
164 }
165
166 func ShowGroups(c gig.Context) (error) {
167 user, exist := db.GetUser(c.CertHash())
168 if !exist {
169 return c.NoContent(gig.StatusBadRequest, "Invalid username")
170 }
171 groups, err := user.GetGroups()
172 if err != nil {
173 log.Println(err.Error())
174 return c.NoContent(gig.StatusTemporaryFailure,
175 "Failed to fetch groups")
176 }
177 data := struct {
178 Groups []db.Group
179 }{
180 Groups: groups,
181 }
182 return execT(c, "group_list.gmi", data)
183 }
184
185 func ShowMembers(c gig.Context) (error) {
186 user, exist := db.GetUser(c.CertHash())
187 if !exist {
188 return c.NoContent(gig.StatusBadRequest, "Invalid username")
189 }
190 group := c.Param("group")
191 isOwner, err := user.IsInGroup(group)
192 if err != nil {
193 return c.NoContent(gig.StatusTemporaryFailure,
194 "Group not found")
195 }
196
197 members, err := user.GetMembers(group)
198 if err != nil {
199 log.Println(err.Error())
200 return c.NoContent(gig.StatusTemporaryFailure,
201 "Failed to fetch group members")
202 }
203 desc, err := db.GetGroupDesc(group)
204 if err != nil {
205 log.Println(err.Error())
206 return c.NoContent(gig.StatusTemporaryFailure,
207 "Failed to fetch group description")
208 }
209
210 owner := ""
211 if isOwner {
212 owner = user.Name
213 } else {
214 m, err := db.GetGroupOwner(group)
215 if err != nil {
216 log.Println(err.Error())
217 return c.NoContent(gig.StatusTemporaryFailure,
218 "Failed to fetch group owner")
219 }
220 owner = m.Name
221 }
222
223 data := struct {
224 Members []db.Member
225 MembersCount int
226 IsOwner bool
227 Owner string
228 Group string
229 Description string
230 }{
231 Members: members,
232 MembersCount: len(members),
233 IsOwner: isOwner,
234 Owner: owner,
235 Group: group,
236 Description: desc,
237 }
238 return execT(c, "group.gmi", data)
239 }
240
241 func getRepo(c gig.Context, owner bool) (string, string, error) {
242 username := ""
243 if owner {
244 user, exist := db.GetUser(c.CertHash())
245 if !exist {
246 return "", "", c.NoContent(gig.StatusBadRequest,
247 "Invalid username")
248 }
249 username = user.Name
250 } else {
251 username = c.Param("user")
252 ret, err := db.IsRepoPublic(c.Param("repo"), c.Param("user"))
253 if !ret {
254 user, exist := db.GetUser(c.CertHash())
255 if exist {
256 err := access.HasReadAccess(c.Param("repo"),
257 c.Param("user"),
258 user.Name)
259 ret = err == nil
260 }
261 }
262 if !ret || err != nil {
263 return "", "", c.NoContent(gig.StatusBadRequest,
264 "No repository called " + c.Param("repo") +
265 " by user " + c.Param("user"))
266 }
267 }
268 return username, c.Param("repo"), nil
269 }
270
271 func hasFile(name string, author string, file string) bool {
272 ret, err := repo.GetFile(name, author, file)
273 if ret != nil && err == nil {
274 return true
275 }
276 return false
277 }
278
279 type commit struct {
280 Message string
281 Info string
282 }
283
284 type file struct {
285 Hash string
286 Info string
287 }
288
289 type branch struct {
290 Name string
291 Info string
292 }
293
294 func showRepoLogs(name string, author string) (string, error) {
295 ret, err := repo.GetCommits(name, author)
296 if ret == nil || err == transport.ErrEmptyRemoteRepository {
297 return "", nil
298 }
299 if err != nil {
300 log.Println(err.Error())
301 return "", errors.New("Corrupted repository")
302 }
303 commits := []commit{}
304 err = ret.ForEach(func(c *object.Commit) error {
305 info := c.Hash.String() + ", by " + c.Author.Name + " on " +
306 c.Author.When.Format("2006-01-02 15:04:05")
307 commits = append(commits, commit{Info: info,
308 Message: c.Message})
309 return nil
310 })
311 return execTemplate("repo_log.gmi", commits)
312 }
313
314 func showRepoFiles(name string, author string) (string, error) {
315 ret, err := repo.GetFiles(name, author)
316 if ret == nil || err == transport.ErrEmptyRemoteRepository {
317 return "", nil
318 }
319 if err != nil {
320 log.Println(err.Error())
321 return "", errors.New("Corrupted repository")
322 }
323 files := []file{}
324 err = ret.ForEach(func(f *object.File) error {
325 info := f.Mode.String() + " " + f.Name +
326 " " + strconv.Itoa(int(f.Size))
327 files = append(files, file{Info: info,
328 Hash: f.Blob.Hash.String()})
329 return nil
330 })
331 return execTemplate("repo_files.gmi", files)
332 }
333
334 func showRepoRefs(name string, author string) (string, error) {
335 refs, err := repo.GetRefs(name, author)
336 if refs == nil || err == transport.ErrEmptyRemoteRepository {
337 return "", nil
338 }
339 if err != nil {
340 log.Println(err)
341 return "", errors.New("Corrupted repository")
342 }
343 branches := []branch{}
344 tags := []branch{}
345 err = refs.ForEach(func(c *plumbing.Reference) error {
346 if c.Type().String() != "hash-reference" ||
347 c.Name().IsRemote() {
348 return nil
349 }
350 var b branch
351 b.Name = c.Name().String()
352 b.Name = b.Name[strings.LastIndex(b.Name, "/") + 1:]
353 b.Info = "last commit on "
354
355 commit, err := repo.GetCommit(name, author, c.Hash())
356 if err != nil {
357 b.Info = "failed to fetch commit"
358 } else {
359 when := commit.Author.When
360 str := fmt.Sprintf(
361 "%d-%02d-%02d %02d:%02d:%02d",
362 when.Year(), int(when.Month()),
363 when.Day(), when.Hour(),
364 when.Minute(), when.Second())
365 b.Info += str + " by " + commit.Author.Name
366 }
367 if c.Name().IsBranch() {
368 branches = append(branches, b)
369 } else {
370 tags = append(tags, b)
371 }
372 return nil
373 })
374 refs.Close()
375 data := struct {
376 Branches []branch
377 Tags []branch
378 }{
379 branches,
380 tags,
381 }
382 return execTemplate("repo_refs.gmi", data)
383 }
384
385 func showRepoLicense(name string, author string) (string, error) {
386 content, err := showRepoFile(author, name, "LICENSE")
387 if err != nil {
388 return "", errors.New("No license found")
389 }
390 return execTemplate("repo_license.gmi", content)
391 }
392
393 func showRepoReadme(name string, author string) (string, error) {
394 content, err := showRepoFile(author, name, "README.gmi")
395 if err != nil {
396 content, err = showRepoFile(author, name, "README")
397 }
398 if err != nil {
399 return "", errors.New("No readme found")
400 }
401 return execTemplate("repo_readme.gmi", content)
402 }
403
404 func showRepo(c gig.Context, page int, owner bool) (error) {
405 author, name, err := getRepo(c, owner)
406 if err != nil {
407 log.Println(err.Error())
408 return c.NoContent(gig.StatusTemporaryFailure, err.Error())
409 }
410 desc, err := db.GetRepoDesc(name, author)
411 if err != nil {
412 log.Println(err.Error())
413 return c.NoContent(gig.StatusTemporaryFailure,
414 "Repository not found")
415 }
416 protocol := "http"
417 if config.Cfg.Git.Https {
418 protocol = "https"
419 }
420 public, err := db.IsRepoPublic(name, author)
421 if err != nil {
422 log.Println(err.Error())
423 return c.NoContent(gig.StatusTemporaryFailure,
424 "Repository not found")
425 }
426
427 content := ""
428 switch page {
429 case pageLog:
430 content, err = showRepoLogs(name, author)
431 case pageFiles:
432 content, err = showRepoFiles(name, author)
433 case pageRefs:
434 content, err = showRepoRefs(name, author)
435 case pageLicense:
436 content, err = showRepoLicense(name, author)
437 case pageReadme:
438 content, err = showRepoReadme(name, author)
439 }
440 if err != nil {
441 return c.NoContent(gig.StatusBadRequest,
442 "Invalid repository")
443 }
444
445 data := struct {
446 Protocol string
447 Domain string
448 User string
449 Description string
450 Repo string
451 Public bool
452 HasReadme bool
453 HasLicense bool
454 Content string
455 }{
456 Protocol: protocol,
457 Domain: config.Cfg.Git.Domain,
458 User: author,
459 Description: desc,
460 Repo: name,
461 Public: public,
462 HasReadme: hasFile(name, author, "README.gmi") ||
463 hasFile(name, author, "README"),
464 HasLicense: hasFile(name, author, "LICENSE"),
465 Content: content,
466 }
467 if owner {
468 return execT(c, "repo.gmi", data)
469 }
470 return execT(c, "public_repo.gmi", data)
471 }
472
473 func PublicList(c gig.Context) (error) {
474 repos, err := db.GetPublicRepo()
475 if err != nil {
476 log.Println(err.Error())
477 return c.NoContent(gig.StatusTemporaryFailure,
478 "Internal error, "+err.Error())
479 }
480 return execT(c, "public_list.gmi", repos)
481 }
482
483 func PublicAccount(c gig.Context) error {
484 user, err := db.GetPublicUser(c.Param("user"))
485 if err != nil {
486 return c.NoContent(gig.StatusBadRequest, err.Error())
487 }
488 repos, err := user.GetRepos(true)
489 if err != nil {
490 return c.NoContent(gig.StatusTemporaryFailure,
491 "Invalid account, " + err.Error())
492 }
493 data := struct {
494 Name string
495 Description string
496 Repositories []db.Repo
497 }{
498 user.Name,
499 user.Description,
500 repos,
501 }
502 return execT(c, "public_user.gmi", data)
503 }
504
505 func ShowAccess(c gig.Context) error {
506 user, exist := db.GetUser(c.CertHash())
507 if !exist {
508 return c.NoContent(gig.StatusBadRequest, "Invalid username")
509 }
510 repo, err := user.GetRepo(c.Param("repo"))
511 if err != nil {
512 return c.NoContent(gig.StatusBadRequest, err.Error())
513 }
514 access, err := db.GetRepoUserAccess(repo.ID)
515 if err != nil {
516 return c.NoContent(gig.StatusBadRequest, err.Error())
517 }
518 groups, err := db.GetRepoGroupAccess(repo.ID)
519 if err != nil {
520 return c.NoContent(gig.StatusBadRequest, err.Error())
521 }
522 data := struct {
523 Repo string
524 Collaborators []db.Access
525 Groups []db.Access
526 Owner bool
527 }{
528 Repo: repo.Name,
529 Collaborators: access,
530 Groups: groups,
531 Owner: true,
532 }
533 return execT(c, "repo_access.gmi", data)
534 }
535