This repository has been archived on 2025-06-10. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
server/admin/api/theme.go

376 lines
10 KiB
Go

package api
import (
"fmt"
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/gin-gonic/gin"
)
func declareThemesRoutes(router *gin.RouterGroup) {
router.GET("/themes", listThemes)
router.POST("/themes", createTheme)
router.GET("/themes.json", exportThemes)
router.GET("/session-forensic.yaml", func(c *gin.Context) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
log.Printf("Unable to ReadSettings: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."})
return
} else if challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, "challenge.json"); err != nil {
log.Println("Unable to retrieve challenge.json:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrive challenge.json: %s", err.Error())})
return
} else if ch, err := settings.ReadChallengeInfo(challengeinfo); err != nil {
log.Printf("Unable to ReadChallengeInfo: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."})
return
} else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil {
log.Printf("Unable to GenZQDSSessionFile: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."})
return
} else {
c.JSON(http.StatusOK, sf)
}
})
router.GET("/files-bindings", bindingFiles)
apiThemesRoutes := router.Group("/themes/:thid")
apiThemesRoutes.Use(ThemeHandler)
apiThemesRoutes.GET("", showTheme)
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
declareExercicesRoutes(apiThemesRoutes)
// Remote
router.GET("/remote/themes", sync.ApiListRemoteThemes)
router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme)
router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices)
}
type Theme struct {
*fic.Theme
ForgeLink string `json:"forge_link,omitempty"`
}
func ThemeHandler(c *gin.Context) {
thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"})
return
}
if thid == 0 {
c.Set("theme", &fic.StandaloneExercicesTheme)
} else {
theme, err := fic.GetTheme(thid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
c.Set("theme", theme)
}
c.Next()
}
func fixAllURLIds(c *gin.Context) {
nbFix := 0
if themes, err := fic.GetThemes(); err == nil {
for _, theme := range themes {
if theme.FixURLId() {
theme.Update()
nbFix += 1
}
if exercices, err := theme.GetExercices(); err == nil {
for _, exercice := range exercices {
if exercice.FixURLId() {
exercice.Update()
nbFix += 1
}
}
}
}
}
c.JSON(http.StatusOK, nbFix)
}
func bindingFiles(c *gin.Context) {
files, err := fic.GetFiles()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
c.String(http.StatusOK, ret)
}
func listThemes(c *gin.Context) {
themes, err := fic.GetThemes()
if err != nil {
log.Println("Unable to listThemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."})
return
}
if has, _ := fic.HasStandaloneExercice(); has {
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
}
c.JSON(http.StatusOK, themes)
}
func exportThemes(c *gin.Context) {
themes, err := fic.ExportThemes()
if err != nil {
log.Println("Unable to exportthemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to export themes."})
return
}
c.JSON(http.StatusOK, themes)
}
func showTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var forgelink string
if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok {
if u, _ := fli.GetThemeLink(theme); u != nil {
forgelink = u.String()
}
}
c.JSON(http.StatusOK, Theme{theme, forgelink})
}
func createTheme(c *gin.Context) {
var ut fic.Theme
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if len(ut.Name) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
th, err := fic.CreateTheme(&ut)
if err != nil {
log.Println("Unable to createTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."})
return
}
c.JSON(http.StatusOK, th)
}
func updateTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var ut fic.Theme
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ut.Id = theme.Id
if len(ut.Name) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
if _, err := ut.Update(); err != nil {
log.Println("Unable to updateTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."})
return
}
if theme.Locked != ut.Locked {
exercices, err := theme.GetExercices()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
for _, exercice := range exercices {
if exercice.Disabled != ut.Locked {
exercice.Disabled = ut.Locked
_, err = exercice.Update()
if err != nil {
log.Println("Unable to enable/disable exercice: ", exercice.Id, err.Error())
}
}
}
}
c.JSON(http.StatusOK, ut)
}
func deleteTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
_, err := theme.Delete()
if err != nil {
log.Println("Unable to deleteTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."})
return
}
c.JSON(http.StatusOK, true)
}
func getThemedExercicesStats(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exercices, err := theme.GetExercices()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to fetch exercices: %s", err.Error())})
return
}
ret := []exerciceStats{}
for _, e := range exercices {
ret = append(ret, exerciceStats{
IdExercice: e.Id,
TeamTries: e.TriedTeamCount(),
TotalTries: e.TriedCount(),
SolvedCount: e.SolvedCount(),
FlagSolved: e.FlagSolved(),
MCQSolved: e.MCQSolved(),
})
}
c.JSON(http.StatusOK, ret)
}
func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) {
var diffs []syncDiff
// Compare theme attributes
theme_remote, err := sync.GetRemoteTheme(theme.Path)
if err != nil {
return nil, err
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) {
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" {
continue
}
diffs = append(diffs, syncDiff{
Field: field.Name,
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(),
})
}
}
// Compare exercices list
exercices, err := theme.GetExercices()
if err != nil {
return nil, fmt.Errorf("Unable to GetExercices: %w", err)
}
exercices_remote, err := sync.ListRemoteExercices(theme.Path)
if err != nil {
return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err)
}
var not_found []string
var extra_found []string
for _, exercice_remote := range exercices_remote {
found := false
for _, exercice := range exercices {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
not_found = append(not_found, exercice_remote)
}
}
for _, exercice := range exercices {
found := false
for _, exercice_remote := range exercices_remote {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:])
}
}
if len(not_found) > 0 || len(extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: "theme.Exercices",
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: strings.Join(extra_found, ", "),
After: strings.Join(not_found, ", "),
})
}
// Compare inner exercices
for i, exercice := range exercices {
exdiffs, err := diffExerciceWithRemote(exercice, theme)
if err != nil {
return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err)
}
for _, exdiff := range exdiffs {
if theme.Id == 0 {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field)
} else {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field)
}
diffs = append(diffs, exdiff)
}
}
return diffs, err
}
func APIDiffThemeWithRemote(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
diffs, err := diffThemeWithRemote(theme)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}