diff --git a/admin/api/exercice.go b/admin/api/exercice.go index 19c746be..7636f29a 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -36,6 +36,11 @@ func init() { router.GET("/api/exercices/:eid/flags/:kid", apiHandler(flagHandler(showExerciceFlag))) router.PUT("/api/exercices/:eid/flags/:kid", apiHandler(flagHandler(updateExerciceFlag))) router.DELETE("/api/exercices/:eid/flags/:kid", apiHandler(flagHandler(deleteExerciceFlag))) + router.GET("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagHandler(listFlagChoices))) + router.GET("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(showFlagChoice))) + router.POST("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagHandler(createFlagChoice))) + router.PUT("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(updateFlagChoice))) + router.DELETE("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(deleteFlagChoice))) router.GET("/api/exercices/:eid/quiz", apiHandler(exerciceHandler(listExerciceQuiz))) router.GET("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(showExerciceQuiz))) @@ -86,6 +91,10 @@ func listExerciceFlags(exercice fic.Exercice, body []byte) (interface{}, error) return exercice.GetFlags() } +func listFlagChoices(flag fic.Flag, _ fic.Exercice, body []byte) (interface{}, error) { + return flag.GetChoices() +} + func listExerciceQuiz(exercice fic.Exercice, body []byte) (interface{}, error) { return exercice.GetMCQ() } @@ -310,6 +319,51 @@ func deleteExerciceFlag(flag fic.Flag, _ fic.Exercice, _ []byte) (interface{}, e return flag.Delete() } +type uploadedChoice struct { + Label string + Value string +} + +func createFlagChoice(flag fic.Flag, exercice fic.Exercice, body []byte) (interface{}, error) { + var uc uploadedChoice + if err := json.Unmarshal(body, &uc); err != nil { + return nil, err + } + + if len(uc.Label) == 0 { + uc.Label = uc.Value + } + + return flag.AddChoice(uc.Label, uc.Value) +} + +func showFlagChoice(choice fic.FlagChoice, _ fic.Exercice, body []byte) (interface{}, error) { + return choice, nil +} + +func updateFlagChoice(choice fic.FlagChoice, _ fic.Exercice, body []byte) (interface{}, error) { + var uc uploadedChoice + if err := json.Unmarshal(body, &uc); err != nil { + return nil, err + } + + if len(uc.Label) == 0 { + choice.Label = uc.Value + } else { + choice.Label = uc.Label + } + + if _, err := choice.Update(); err != nil { + return nil, err + } + + return choice, nil +} + +func deleteFlagChoice(choice fic.FlagChoice, _ fic.Exercice, _ []byte) (interface{}, error) { + return choice.Delete() +} + func showExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, body []byte) (interface{}, error) { return quiz, nil } diff --git a/admin/api/handlers.go b/admin/api/handlers.go index 0d6b22fb..feac7009 100644 --- a/admin/api/handlers.go +++ b/admin/api/handlers.go @@ -188,6 +188,26 @@ func flagHandler(f func(fic.Flag, fic.Exercice, []byte) (interface{}, error)) fu } } +func choiceHandler(f func(fic.FlagChoice, fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { + return func(ps httprouter.Params, body []byte) (interface{}, error) { + var exercice fic.Exercice + var flag fic.Flag + flagHandler(func(fl fic.Flag, ex fic.Exercice, _ []byte) (interface{}, error) { + exercice = ex + flag = fl + return nil, nil + })(ps, body) + + if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil { + return nil, err + } else if choice, err := flag.GetChoice(cid); err != nil { + return nil, err + } else { + return f(choice, exercice, body) + } + } +} + func quizHandler(f func(fic.MCQ, fic.Exercice, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) { var exercice fic.Exercice diff --git a/admin/sync/exercice_keys.go b/admin/sync/exercice_keys.go index 06519a5c..4f0c8d6c 100644 --- a/admin/sync/exercice_keys.go +++ b/admin/sync/exercice_keys.go @@ -90,6 +90,26 @@ func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { continue } } + + // Import choices + hasOne := false + for cid, choice := range flag.Choice { + if len(choice.Label) == 0 { + choice.Label = choice.Value + } + + if _, err := k.AddChoice(choice.Label, choice.Value); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in UCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err)) + continue + } + + if choice.Value == flag.Raw { + hasOne = true + } + } + if !hasOne { + errs = append(errs, fmt.Sprintf("%q: error in UCQ %d: no valid answer defined.", path.Base(exercice.Path), nline + 1)) + } } } diff --git a/libfic/db.go b/libfic/db.go index b446805f..1180b0fe 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -168,6 +168,17 @@ CREATE TABLE IF NOT EXISTS exercice_flags( cksum BINARY(64) NOT NULL, FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice) ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS flag_choices( + id_choice INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + id_flag INTEGER NOT NULL, + label VARCHAR(255) NOT NULL, + response VARCHAR(255) NOT NULL, + FOREIGN KEY(id_flag) REFERENCES exercice_flags(id_flag) +) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; `); err != nil { return err } diff --git a/libfic/flag.go b/libfic/flag.go index 197aa998..e13b8e60 100644 --- a/libfic/flag.go +++ b/libfic/flag.go @@ -118,6 +118,8 @@ func (k Flag) Update() (int64, error) { func (k Flag) Delete() (int64, error) { if _, err := DBExec("DELETE FROM exercice_files_deps WHERE id_flag = ?", k.Id); err != nil { return 0, err + } else if _, err := DBExec("DELETE FROM flag_choices WHERE id_flag = ?", k.Id); err != nil { + return 0, err } else if res, err := DBExec("DELETE FROM exercice_flags WHERE id_flag = ?", k.Id); err != nil { return 0, err } else if nb, err := res.RowsAffected(); err != nil { @@ -131,6 +133,8 @@ func (k Flag) Delete() (int64, error) { func (e Exercice) WipeFlags() (int64, error) { if _, err := DBExec("DELETE FROM exercice_files_deps WHERE id_flag IN (SELECT id_flag FROM exercice_flags WHERE id_exercice = ?)", e.Id); err != nil { return 0, err + } else if _, err := DBExec("DELETE FROM flag_choices WHERE id_flag IN (SELECT id_flag FROM exercice_flags WHERE id_exercice = ?)", e.Id); err != nil { + return 0, err } else if res, err := DBExec("DELETE FROM exercice_flags WHERE id_exercice = ?", e.Id); err != nil { return 0, err } else if nb, err := res.RowsAffected(); err != nil { diff --git a/libfic/flag_choice.go b/libfic/flag_choice.go new file mode 100644 index 00000000..86070c63 --- /dev/null +++ b/libfic/flag_choice.go @@ -0,0 +1,92 @@ +package fic + +import () + +// FlagChoice represents a choice a respond to a classic flag +type FlagChoice struct { + Id int64 `json:"id"` + // IdFlag is the identifier of the underlying flag + IdFlag int64 `json:"idFlag"` + // Label is the title of the choice as displayed to players + Label string `json:"label"` + // Value is the raw content that'll be written as response if this choice is selected + Value string `json:"value"` +} + +// GetChoices returns a list of choices for the given Flag. +func (f Flag) GetChoices() ([]FlagChoice, error) { + if rows, err := DBQuery("SELECT id_choice, id_flag, label, response FROM flag_choices WHERE id_flag = ?", f.Id); err != nil { + return nil, err + } else { + defer rows.Close() + + var choices = make([]FlagChoice, 0) + for rows.Next() { + var c FlagChoice + c.IdFlag = f.Id + + if err := rows.Scan(&c.Id, &c.IdFlag, &c.Label, &c.Value); err != nil { + return nil, err + } + + choices = append(choices, c) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return choices, nil + } +} + +// GetChoice returns a choice for the given Flag. +func (f Flag) GetChoice(id int) (c FlagChoice, err error) { + if errr := DBQueryRow("SELECT id_choice, id_flag, label, response FROM flag_choices WHERE id_choice = ?", id).Scan(&c.Id, &c.IdFlag, &c.Label, &c.Value); errr != nil { + return c, errr + } + return +} + +// AddChoice creates and fills a new struct FlagChoice, from a label and a value. +func (f Flag) AddChoice(label string, value string) (FlagChoice, error) { + if res, err := DBExec("INSERT INTO flag_choices (id_flag, label, response) VALUES (?, ?, ?)", f.Id, label, value); err != nil { + return FlagChoice{}, err + } else if cid, err := res.LastInsertId(); err != nil { + return FlagChoice{}, err + } else { + return FlagChoice{cid, f.Id, label, value}, nil + } +} + +// Update applies modifications back to the database. +func (c FlagChoice) Update() (int64, error) { + if res, err := DBExec("UPDATE flag_choices SET id_flag = ?, label = ?, value = ? WHERE id_choice = ?", c.IdFlag, c.Label, c.Value, c.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + +// Delete the flag from the database. +func (c FlagChoice) Delete() (int64, error) { + if res, err := DBExec("DELETE FROM flag_choices WHERE id_choice = ?", c.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + +// WipeFlags deletes flags coming with the challenge. +func (f Flag) WipeChoices() (int64, error) { + if res, err := DBExec("DELETE FROM flag_choices WHERE id_flag = ?", f.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} diff --git a/libfic/reset.go b/libfic/reset.go index a738e077..62d9038f 100644 --- a/libfic/reset.go +++ b/libfic/reset.go @@ -34,7 +34,7 @@ func ResetGame() (error) { // ResetExercices wipes out all challenges (both attempts and statements). func ResetExercices() (error) { - return truncateTable("team_hints", "exercice_files_deps", "exercice_files", "flag_found", "exercice_flags", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercice_tags", "exercices", "themes") + return truncateTable("team_hints", "exercice_files_deps", "exercice_files", "flag_found", "flag_choices", "exercice_flags", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercice_tags", "exercices", "themes") } // ResetTeams wipes out all teams, incluings members and attempts.