package sync import ( "fmt" "math" "math/rand" "net/http" "path" "sort" "strconv" "strings" "unicode" "github.com/gin-gonic/gin" "go.uber.org/multierr" "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, separator string) (raw string, prep string, errs error) { // Concatenate array if f, ok := input.([]interface{}); ok { if len(validatorRe) > 0 { errs = multierr.Append(errs, fmt.Errorf("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 = multierr.Append(errs, fmt.Errorf("separator truncated to %q", separator)) } var fitems []string for i, v := range f { if g, ok := v.(string); ok { if strings.Index(g, separator) != -1 { errs = multierr.Append(errs, fmt.Errorf("flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.", separator)) return } else { fitems = append(fitems, g) } } else { errs = multierr.Append(errs, fmt.Errorf("item %d has an invalid type: can only be string, is %T.", i, g)) return } } ignord := "f" if !ordered { // Sort the list without taking the case in count. sort.Slice(fitems, func(i, j int) bool { return strings.ToLower(fitems[i]) < strings.ToLower(fitems[j]) }) ignord = "t" } nbLines := 0 if showLines { if len(fitems) > 9 { errs = multierr.Append(errs, fmt.Errorf("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.(float64); ok { raw = strconv.FormatFloat(f, 'f', -1, 64) } else if f, ok := input.(string); !ok { errs = multierr.Append(errs, fmt.Errorf("has an invalid type: can only be []string or string, not %T", input)) return } else { raw = f } return } func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exceptions *CheckExceptions) (f *fic.FlagLabel, errs error) { if len(flag.Label) == 0 { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label cannot be empty."))) return } // Call checks hooks for _, h := range hooks.mdTextHooks { for _, err := range multierr.Errors(h(flag.Label, exercice.Language, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, err)) } } if mdlabel, err := ProcessMarkdown(GlobalImporter, flag.Label, exercice.Path); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("unable to parse property label as Markdown: %w", err))) } else { if strings.Count(flag.Label, "\n\n") == 0 { flag.Label = mdlabel[3 : len(mdlabel)-4] } else { flag.Label = mdlabel } } if flag.Raw != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("raw cannot be defined."))) } if len(flag.Choice) != 0 { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("choices cannot be defined."))) } f = &fic.FlagLabel{ Order: int8(flagline), Label: flag.Label, Variant: flag.Variant, } // Call checks hooks for _, h := range hooks.flagLabelHooks { for _, e := range multierr.Errors(h(f, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e)) } } return } func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string, exceptions *CheckExceptions) (f *fic.Flag, choices []*fic.FlagChoice, errs error) { if len(flag.Label) == 0 { flag.Label = defaultLabel } if len(flag.Variant) != 0 { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("variant is not defined for this kind of flag."))) } if flag.Label[0] == '`' { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label should not begin with `."))) flag.Label = flag.Label[1:] } raw, prep, terrs := getRawKey(flag.Raw, flag.CaptureRe, flag.Ordered, flag.ShowLines, flag.Separator) errors := multierr.Errors(terrs) if len(errors) > 0 { for _, terr := range errors { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, terr)) } f = nil return } flag.Label = prep + flag.Label if len(flag.Label) > 255 && flag.Type != "label" { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("label is too long (max 255 chars per label)."))) } if (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !isFullGraphic(raw)) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("WARNING non-printable characters in flag, is this really expected?"))) } hashedFlag, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, flag.NoTrim, validatorRegexp(flag.CaptureRe), flag.SortReGroups) if err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, err)) return } fk := &fic.FlagKey{ Type: flag.Type, IdExercice: exercice.Id, Order: int8(flagline), Label: flag.Label, Placeholder: flag.Placeholder, Help: flag.Help, Unit: flag.Unit, IgnoreCase: !flag.CaseSensitive, Multiline: flag.Type == "text", CaptureRegexp: validatorRegexp(flag.CaptureRe), SortReGroups: flag.SortReGroups, Checksum: hashedFlag[:], ChoicesCost: flag.ChoicesCost, BonusGain: flag.BonusGain, } // Call checks hooks for _, h := range hooks.flagKeyHooks { for _, e := range multierr.Errors(h(fk, raw, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e)) } } fl := fic.Flag(fk) f = &fl if len(flag.Choice) > 0 || (flag.Type == "ucq" || flag.Type == "radio") { // 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, "") errors := multierr.Errors(terrs) if len(errors) > 0 { for _, terr := range errors { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, terr)) } continue } if len(choice.Label) == 0 { choice.Label = val } choice.Label = prep + choice.Label if !flag.CaseSensitive { val = strings.ToLower(val) } fc := &fic.FlagChoice{ Label: choice.Label, Value: val, } // Call checks hooks for _, h := range hooks.flagChoiceHooks { for _, e := range multierr.Errors(h(fc, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e)) } } choices = append(choices, fc) if val == "true" || val == "false" { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("value can't be %q, this is not a MCQ, the value has to be meaningful. The value is shown to players as response identifier.", val))) } if val == raw || (!flag.CaseSensitive && val == strings.ToLower(raw)) { hasOne = true } } if !hasOne { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("no valid answer defined."))) } // Call checks hooks for _, h := range hooks.flagKeyWithChoicesHooks { for _, e := range multierr.Errors(h(fk, raw, choices, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e)) } } } return } type importFlag struct { Line int Flag fic.Flag JustifyOf *fic.MCQ_entry Choices []*fic.FlagChoice FilesDeps []string FlagsDeps []int64 } func iface2Number(input interface{}, output *string) (norm float64, err error) { if input != nil { if v, ok := input.(int64); ok { *output = fmt.Sprintf("%d", v) norm = float64(v) } else if v, ok := input.(float64); ok { *output = strconv.FormatFloat(v, 'f', -1, 64) norm = v } else { err = fmt.Errorf("has an invalid type: expected int or float, got %T", input) } } return } // buildExerciceFlag read challenge.txt and extract all flags. func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nline int, exceptions *CheckExceptions) (ret []importFlag, errs error) { switch strings.ToLower(flag.Type) { case "": flag.Type = "key" case "label": flag.Type = "label" case "key": flag.Type = "key" case "number": var smin, smax, sstep string fstep, err := iface2Number(flag.NumberStep, &sstep) if err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("step %w", err))) } _, err = iface2Number(flag.NumberMin, &smin) if err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("min %w", err))) } _, err = iface2Number(flag.NumberMax, &smax) if err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("max %w", err))) } // Ensure step permit validating the flag if rns, err := flag.RawNumber(); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("raw %w", err))) } else { if fstep == 0 { fstep = 1.0 } for _, rn := range rns { v := math.Abs(rn) / fstep if float64(int(v)) != v { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("choosen step=%f doesn't include response=%f", fstep, rn))) } } } flag.Type = fmt.Sprintf("number,%s,%s,%s", smin, smax, sstep) case "text": flag.Type = "text" case "vector": flag.Type = "vector" case "ucq": flag.Type = "ucq" case "radio": flag.Type = "radio" case "mcq": flag.Type = "mcq" case "justified": flag.Type = "justified" default: errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'justified', 'ucq', 'radio' or 'vector'"))) return } if !strings.HasPrefix(flag.Type, "number") { if flag.NumberMin != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property min undefined for this kind of flag: should the type be 'number'"))) } else if flag.NumberMax != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property max undefined for this kind of flag: should the type be 'number'"))) } else if flag.NumberStep != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property step undefined for this kind of flag: should the type be 'number'"))) } } if len(flag.Help) > 0 { // Call checks hooks for _, hk := range hooks.mdTextHooks { for _, err := range multierr.Errors(hk(flag.Help, exercice.Language, exceptions)) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, err)) } } if mdhelp, err := ProcessMarkdown(i, flag.Help, exercice.Path); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("unable to parse property help as Markdown: %w", err))) } else { flag.Help = mdhelp[3 : len(mdhelp)-4] } } if flag.Type == "label" { addedFlag, berrs := buildLabelFlag(exercice, flag, nline+1, exceptions) errs = multierr.Append(errs, berrs) if addedFlag != nil { ret = append(ret, importFlag{ Line: nline + 1, Flag: addedFlag, }) } } else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" { addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag", exceptions) errs = multierr.Append(errs, berrs) if addedFlag != nil { ret = append(ret, importFlag{ Line: nline + 1, Flag: *addedFlag, Choices: choices, }) } } else if flag.Type == "mcq" || flag.Type == "justified" { addedFlag := fic.MCQ{ IdExercice: exercice.Id, Order: int8(nline + 1), Title: flag.Label, Entries: []*fic.MCQ_entry{}, } hasOne := false isJustified := flag.Type == "justified" if len(flag.Variant) != 0 { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag"))) } 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 = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ"))) continue } val = true isJustified = true } else if p, ok := choice.Value.(bool); ok { val = p if isJustified { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ"))) continue } } else if choice.Value == nil { val = false } else { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("choice %d: incorrect type for value: %T is not boolean.", cid, choice.Value))) continue } entry := &fic.MCQ_entry{ Label: choice.Label, Response: val, } addedFlag.Entries = append(addedFlag.Entries, entry) if isJustified && choice.Raw != nil { addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant", exceptions) errs = multierr.Append(errs, berrs) if addedFlag != nil { ret = append(ret, importFlag{ Line: nline + 1, Flag: *addedFlag, JustifyOf: entry, Choices: choices, }) } } } // Call checks hooks for _, h := range hooks.flagMCQHooks { for _, e := range multierr.Errors(h(&addedFlag, addedFlag.Entries, exercice, exceptions)) { errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, e)) } } ret = append([]importFlag{importFlag{ Line: nline + 1, Flag: &addedFlag, }}, ret...) } return } // buildExerciceFlags read challenge.txt and extract all flags. func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (flags map[int64]importFlag, flagids []int64, errs error) { params, gerrs := getExerciceParams(i, exercice) if len(multierr.Errors(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 = multierr.Append(errs, NewFlagError(exercice, ¶ms.Flags[nline], nline+1, fmt.Errorf("identifier already used (%d), using a random one.", flag.Id))) flag.Id = rand.Int63() } newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline, exceptions) errs = multierr.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 { if len(nf.Theme) > 0 { errs = multierr.Append(errs, NewFlagError(exercice, ¶ms.Flags[nline], nline+1, fmt.Errorf("dependancy on another scenario is not implemented yet."))) } newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id) } for _, nf := range flag.NeedFlags { newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf) } // 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, exceptions *CheckExceptions) (rf []fic.Flag, errs error) { exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions) errs = multierr.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 = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on flag id=%d: id not defined", nf))) } } if fk, ok := flag.Flag.(*fic.FlagKey); ok { // Check dependency to flag optional flag if fk.BonusGain == 0 { for _, nf := range flag.FlagsDeps { if fk2, ok := flags[nf].Flag.(*fic.FlagKey); ok && fk2.BonusGain != 0 { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag is not optional but depend on flag id=%d which is optional", nf))) } } } if int64(fk.ChoicesCost) >= exercice.Gain { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag's choice_cost is higher than exercice gain"))) } } // Check dependency loop deps := flag.FlagsDeps for i := 0; i < len(deps); i++ { if deps[i] == flagid { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag dependency loop detected: flag id=%d: depends on itself", flagid))) break } deploppadd: for _, d := range flags[deps[i]].FlagsDeps { for _, dd := range deps { if dd == d { continue deploppadd } } deps = append(deps, d) } } // Check dependency to file for _, lf := range flag.FilesDeps { found := false for _, f := range files { if f == lf { found = true break } } if !found { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on %s: No such file", 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, nil) 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, exceptions *CheckExceptions) (kmap map[int64]fic.Flag, errs error) { if _, err := exercice.WipeFlags(); err != nil { errs = multierr.Append(errs, err) } else if _, err := exercice.WipeMCQs(); err != nil { errs = multierr.Append(errs, err) } else { exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions) errs = multierr.Append(errs, berrs) kmap = map[int64]fic.Flag{} // Import flags for _, flagid := range flagids { if flag, ok := flags[flagid]; ok { if flag.JustifyOf != nil { if f, ok := flag.Flag.(*fic.FlagKey); ok { f.Label = fmt.Sprintf("%%%d%%%s", flag.JustifyOf.Id, f.Label) } } if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, err)) } else { if f, ok := addedFlag.(*fic.FlagKey); ok { for _, choice := range flag.Choices { if _, err := f.AddChoice(choice); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("choice #FIXME: %w", err))) } } } kmap[flagid] = addedFlag // Import dependency to flag for _, nf := range flag.FlagsDeps { if rf, ok := kmap[nf]; !ok { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to flag id=%d: id not defined, perhaps not available at time of processing", nf))) } else if err := addedFlag.AddDepend(rf); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to id=%d: %w", nf, err))) } } // Import dependency to file for _, lf := range flag.FilesDeps { if rf, err := exercice.GetFileByFilename(lf); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err))) } else if err := rf.AddDepend(addedFlag); err != nil { errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err))) } } } } } } return } // ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags. func ApiGetRemoteExerciceFlags(c *gin.Context) { theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions) if exercice != nil { flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions) if flags != nil { c.JSON(http.StatusOK, flags) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) return }