server/admin/sync/exercice_keys.go

466 lines
13 KiB
Go

package sync
import (
"fmt"
"math/rand"
"path"
"sort"
"strings"
"unicode"
"github.com/julienschmidt/httprouter"
"srs.epita.fr/fic-server/libfic"
)
// isFullGraphic detects if some rune are not graphic one.
// This function is usefull to display warning when importing key ending with \r.
func isFullGraphic(s string) bool {
for _, c := range s {
if !unicode.IsGraphic(c) {
return false
}
}
return true
}
func validatorRegexp(vre string) (validator_regexp *string) {
if len(vre) > 0 {
validator_regexp = &vre
} else {
validator_regexp = nil
}
return
}
func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bool) (raw string, prep string, errs []string) {
separator := ","
// Concatenate array
if f, ok := input.([]interface{}); ok {
if len(validatorRe) > 0 {
errs = append(errs, "ValidatorRe cannot be defined for this kind of flag.")
validatorRe = ""
}
if len(separator) == 0 {
separator = ","
} else if len(separator) > 1 {
separator = string(separator[0])
errs = append(errs, "separator truncated to %q")
}
var fitems []string
for _, v := range f {
if g, ok := v.(string); ok {
if strings.Index(g, separator) != -1 {
errs = append(errs, "flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.")
return
} else {
fitems = append(fitems, g)
}
} else {
errs = append(errs, "item %d has an invalid type: can only be string, is %T.")
return
}
}
ignord := "f"
if !ordered {
sort.Strings(fitems)
ignord = "t"
}
nbLines := 0
if showLines {
if len(fitems) > 9 {
errs = append(errs, "too much items in vector to use ShowLines features, max 9.")
} else {
nbLines = len(fitems)
}
}
raw = strings.Join(fitems, separator) + separator
prep = fmt.Sprintf("`%s%s%d", separator, ignord, nbLines)
} else if f, ok := input.(int64); ok {
raw = fmt.Sprintf("%d", f)
} else if f, ok := input.(string); !ok {
errs = append(errs, fmt.Sprintf("has an invalid type: can only be []string or string, not %T", input))
return
} else {
raw = f
}
return
}
func buildKeyFlag(exercice fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string) (f *fic.Flag, choices []fic.FlagChoice, errs []string) {
if len(flag.Label) == 0 {
flag.Label = defaultLabel
}
if flag.Label[0] == '`' {
errs = append(errs, fmt.Sprintf("%q: flag #%d: Label should not begin with `.", path.Base(exercice.Path), flagline))
flag.Label = flag.Label[1:]
}
if flag.Label[len(flag.Label)-1] != ')' && flag.Label[len(flag.Label)-1] != '©' && !unicode.IsLetter(rune(flag.Label[len(flag.Label)-1])) && !unicode.IsDigit(rune(flag.Label[len(flag.Label)-1])) {
errs = append(errs, fmt.Sprintf("%q: flag #%d: Label should not end with punct (%q). Reword your label as a description of the expected flag, `:` are automatically appended.", path.Base(exercice.Path), flagline, flag.Label[len(flag.Label)-1]))
}
raw, prep, terrs := getRawKey(flag.Raw, flag.ValidatorRe, flag.Ordered, flag.ShowLines)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), flagline, err))
}
f = nil
return
}
flag.Label = prep + flag.Label
if (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !isFullGraphic(raw)) {
errs = append(errs, fmt.Sprintf("%q: WARNING flag #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), flagline))
}
hashedFlag, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, validatorRegexp(flag.ValidatorRe))
if err != nil {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), flagline, err.Error()))
return
}
fl := fic.Flag(fic.FlagKey{
IdExercice: exercice.Id,
Label: flag.Label,
Placeholder: flag.Placeholder,
IgnoreCase: !flag.CaseSensitive,
Multiline: flag.Type == "text",
ValidatorRegexp: validatorRegexp(flag.ValidatorRe),
Checksum: hashedFlag[:],
ChoicesCost: flag.ChoicesCost,
})
f = &fl
if len(flag.Choice) > 0 || flag.Type == "ucq" {
// Import choices
hasOne := false
if !flag.NoShuffle {
rand.Shuffle(len(flag.Choice), func(i, j int) {
flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
})
}
for _, choice := range flag.Choice {
val, prep, terrs := getRawKey(choice.Value, "", false, false)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), flagline, err))
}
continue
}
if len(choice.Label) == 0 {
choice.Label = val
}
choice.Label = prep + choice.Label
choices = append(choices, fic.FlagChoice{
Label: choice.Label,
Value: val,
})
if val == raw || (!flag.CaseSensitive && val == strings.ToLower(raw)) {
hasOne = true
}
}
if !hasOne {
errs = append(errs, fmt.Sprintf("%q: error in flag #%d: no valid answer defined.", path.Base(exercice.Path), flagline))
}
}
return
}
type importFlag struct {
Line int
Flag fic.Flag
Choices []fic.FlagChoice
FilesDeps []string
FlagsDeps []int64
}
// buildExerciceFlags read challenge.txt and extract all flags.
func buildExerciceFlag(i Importer, exercice fic.Exercice, flag ExerciceFlag, nline int) (ret []importFlag, errs []string) {
switch strings.ToLower(flag.Type) {
case "":
flag.Type = "key"
case "key":
flag.Type = "key"
case "text":
flag.Type = "text"
case "vector":
flag.Type = "vector"
case "ucq":
flag.Type = "ucq"
case "mcq":
flag.Type = "mcq"
default:
errs = append(errs, fmt.Sprintf("%q: flag #%d: invalid type of flag: should be 'key', 'text', 'mcq', 'ucq' or 'vector'.", path.Base(exercice.Path), nline+1))
return
}
if flag.Type == "key" || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "vector" {
addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag")
if len(berrs) > 0 {
errs = append(errs, berrs...)
}
if addedFlag != nil {
ret = append(ret, importFlag{
Line: nline + 1,
Flag: *addedFlag,
Choices: choices,
})
}
} else if flag.Type == "mcq" {
addedFlag := fic.MCQ{
IdExercice: exercice.Id,
Title: flag.Label,
Entries: []fic.MCQ_entry{},
}
hasOne := false
isJustified := false
if !flag.NoShuffle {
rand.Shuffle(len(flag.Choice), func(i, j int) {
flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
})
}
for cid, choice := range flag.Choice {
var val bool
if choice.Raw != nil {
if hasOne && !isJustified {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline+1))
continue
}
val = true
isJustified = true
} else if p, ok := choice.Value.(bool); ok {
val = p
if isJustified {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline+1))
continue
}
} else if choice.Value == nil {
val = false
} else {
errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: incorrect type for value: %T is not boolean.", path.Base(exercice.Path), nline+1, cid, choice.Value))
continue
}
addedFlag.Entries = append(addedFlag.Entries, fic.MCQ_entry{
Label: choice.Label,
Response: val,
})
if isJustified && choice.Raw != nil {
addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant")
if len(berrs) > 0 {
errs = append(errs, berrs...)
}
if addedFlag != nil {
ret = append(ret, importFlag{
Line: nline + 1,
Flag: *addedFlag,
Choices: choices,
})
}
}
}
ret = append(ret, importFlag{
Line: nline + 1,
Flag: addedFlag,
})
}
return
}
// buildExerciceFlags read challenge.txt and extract all flags.
func buildExerciceFlags(i Importer, exercice fic.Exercice) (flags map[int64]importFlag, flagids []int64, errs []string) {
params, gerrs := getExerciceParams(i, exercice)
if len(gerrs) > 0 {
return flags, flagids, gerrs
}
flags = map[int64]importFlag{}
for nline, flag := range params.Flags {
if flag.Id == 0 {
// TODO: should be more smart than that. Perhaps search to increment if possible.
flag.Id = rand.Int63()
}
// Ensure flag ID is unique
for _, ok := flags[flag.Id]; ok; _, ok = flags[flag.Id] {
errs = append(errs, fmt.Sprintf("%q: flag #%d: identifier already used (%d), using a random one.", path.Base(exercice.Path), nline+1, flag.Id))
flag.Id = rand.Int63()
}
newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline)
if len(ferrs) > 0 {
errs = append(errs, ferrs...)
}
if len(newFlags) > 0 {
for _, newFlag := range newFlags {
fId := flag.Id
for _, ok := flags[fId]; ok; _, ok = flags[fId] {
fId = rand.Int63()
}
// Read dependency to flag
for _, nf := range flag.NeedFlag {
newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id)
}
// Read dependency to file
for _, lf := range flag.LockedFile {
newFlag.FilesDeps = append(newFlag.FilesDeps, lf.Filename)
}
flags[fId] = newFlag
flagids = append(flagids, fId)
}
}
}
return
}
// CheckExerciceFlags checks if all flags for the given challenge are correct.
func CheckExerciceFlags(i Importer, exercice fic.Exercice, files []string) (rf []fic.Flag, errs []string) {
flags, flagsids, berrs := buildExerciceFlags(i, exercice)
errs = append(errs, berrs...)
for _, flagid := range flagsids {
if flag, ok := flags[flagid]; ok {
// Check dependency to flag
for _, nf := range flag.FlagsDeps {
if _, ok := flags[nf]; !ok {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined", path.Base(exercice.Path), flag.Line, nf))
}
}
// Check dependency to file
for _, lf := range flag.FilesDeps {
found := false
for _, f := range files {
if f == lf {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: No such file", path.Base(exercice.Path), flag.Line, lf))
}
}
rf = append(rf, flag.Flag)
}
}
return
}
// ExerciceFlagsMap builds the flags bindings between challenge.txt and DB.
func ExerciceFlagsMap(i Importer, exercice fic.Exercice) (kmap map[int64]fic.Flag) {
flags, flagids, _ := buildExerciceFlags(i, exercice)
kmap = map[int64]fic.Flag{}
for _, flagid := range flagids {
if flag, ok := flags[flagid]; ok {
if addedFlag, err := flag.Flag.RecoverId(); err == nil {
kmap[flagid] = addedFlag
}
}
}
return
}
// SyncExerciceFlags imports all kind of flags for the given challenge.
func SyncExerciceFlags(i Importer, exercice fic.Exercice) (kmap map[int64]fic.Flag, errs []string) {
if _, err := exercice.WipeFlags(); err != nil {
errs = append(errs, err.Error())
} else if _, err := exercice.WipeMCQs(); err != nil {
errs = append(errs, err.Error())
} else {
flags, flagids, berrs := buildExerciceFlags(i, exercice)
errs = append(errs, berrs...)
kmap = map[int64]fic.Flag{}
// Import flags
for _, flagid := range flagids {
if flag, ok := flags[flagid]; ok {
if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), flag.Line, err))
} else {
if f, ok := addedFlag.(fic.FlagKey); ok {
for _, choice := range flag.Choices {
if _, err := f.AddChoice(choice); err != nil {
errs = append(errs, fmt.Sprintf("%q: error in flag #%d choice #FIXME: %s", path.Base(exercice.Path), flag.Line, err))
}
}
}
kmap[flagid] = addedFlag
// Import dependency to flag
for _, nf := range flag.FlagsDeps {
if rf, ok := kmap[nf]; !ok {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined, perhaps not available at time of processing", path.Base(exercice.Path), flag.Line, nf))
} else if err := addedFlag.AddDepend(rf); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to id=%d: %s", path.Base(exercice.Path), flag.Line, nf, err))
}
}
// Import dependency to file
for _, lf := range flag.FilesDeps {
if rf, err := exercice.GetFileByFilename(lf); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), flag.Line, lf, err))
} else if err := rf.AddDepend(addedFlag); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), flag.Line, lf, err))
}
}
}
}
}
}
return
}
// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(ps httprouter.Params, _ []byte) (interface{}, error) {
theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid"))
if theme != nil {
exercice, _, _, _, errs := BuildExercice(GlobalImporter, *theme, path.Join(theme.Path, ps.ByName("exid")), nil)
if exercice != nil {
flags, errs := CheckExerciceFlags(GlobalImporter, *exercice, []string{})
if flags != nil {
return flags, nil
} else {
return flags, fmt.Errorf("%q", errs)
}
} else {
return exercice, fmt.Errorf("%q", errs)
}
} else {
return nil, fmt.Errorf("%q", errs)
}
}