package view import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "local/dndex/config" "local/dndex/storage" "local/dndex/storage/entity" "local/dndex/storage/operator" "net/http" "net/url" "path" "regexp" "sort" "strings" "github.com/buger/jsonparser" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" "github.com/iancoleman/orderedmap" "go.mongodb.org/mongo-driver/bson" ) const ( querySort = "sort" queryOrder = "order" ) func who(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { namespace, err := getNamespace(r) if err != nil { http.NotFound(w, r) return nil } switch r.Method { case http.MethodGet: return whoGet(namespace, g, w, r) case http.MethodPut: return whoPut(namespace, g, w, r) case http.MethodPost: return whoPost(namespace, g, w, r) case http.MethodDelete: return whoDelete(namespace, g, w, r) case http.MethodPatch: return whoPatch(namespace, g, w, r) case http.MethodTrace: return whoTrace(namespace, g, w, r) default: http.NotFound(w, r) return nil } } func whoGet(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getCleanID(r) if err != nil { return whoTrace(namespace, g, w, r) } _, light := r.URL.Query()["light"] _, md := r.URL.Query()["md"] ones, err := g.ListCaseInsensitive(r.Context(), namespace, id) if err != nil { return err } if len(ones) == 0 { w.WriteHeader(http.StatusNotFound) return json.NewEncoder(w).Encode(entity.One{}) } if len(ones) > 1 { return fmt.Errorf("more than one result found matching %q: %+v", id, ones) } one := ones[0] baseUrl := *r.URL if baseUrl.Scheme == "" { baseUrl.Scheme = "http" } if baseUrl.Host == "" { baseUrl.Host = r.Host } baseUrl.Path = "" baseUrl.RawQuery = "" for k := range one.Attachments { if _, err := url.Parse(one.Attachments[k]); err != nil || !strings.Contains(one.Attachments[k], ":") { one.Attachments[k] = path.Join(baseUrl.String(), config.New().FilePrefix, namespace, one.Attachments[k]) } } if !light && len(one.Connections) > 0 { ones, err := g.ListCaseInsensitive(r.Context(), namespace, one.Peers()...) if err != nil { return err } for _, another := range ones { another.Relationship = one.Connections[another.Name].Relationship one.Connections[another.Name] = another } } b, err := json.MarshalIndent(one, "", " ") if err != nil { return err } if _, ok := r.URL.Query()[querySort]; ok { m := bson.M{} if err := json.Unmarshal(b, &m); err != nil { return err } ones := make([]entity.One, len(one.Connections)) i := 0 for _, v := range one.Connections { ones[i] = v i++ } m[entity.Connections] = sortOnesObject(ones, r) b, err = json.MarshalIndent(m, "", " ") if err != nil { return err } } if md { m := bson.M{} if err := json.Unmarshal(b, &m); err != nil { return err } renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.TOC}) parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock) m["md"] = string(markdown.ToHTML([]byte(one.Text), parser, renderer)) b, err = json.MarshalIndent(m, "", " ") if err != nil { return err } } _, err = io.Copy(w, bytes.NewReader(b)) return err } func whoPut(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getID(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } body, err := ioutil.ReadAll(r.Body) if err != nil { return err } operation := entity.One{} if err := json.Unmarshal(body, &operation); err != nil { return err } if operation.Name != "" && operation.Name != id { http.Error(w, `{"error":"names differ between URL and request body"}`, http.StatusBadRequest) return nil } if operation.Modified != 0 { http.Error(w, `{"error":"cannot specify modified in request body"}`, http.StatusBadRequest) return nil } b, err := bson.Marshal(operation) if err != nil { return err } op := bson.M{} if err := bson.Unmarshal(b, &op); err != nil { return err } for k := range op { if _, _, _, err := jsonparser.Get(body, k); err != nil { delete(op, k) } } if err := g.Update(r.Context(), namespace, entity.One{Name: id}, operator.SetMany{Value: op}); err != nil { return err } return whoGet(namespace, g, w, r) } func whoPost(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getID(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } one := entity.One{} if err := json.NewDecoder(r.Body).Decode(&one); err != nil { return err } if one.Name != "" && one.Name != id { http.Error(w, `{"error":"names differ between URL and request body"}`, http.StatusBadRequest) return nil } one.Name = id if err := g.Insert(r.Context(), namespace, one); err != nil { return err } return whoGet(namespace, g, w, r) } func whoDelete(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getCleanID(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } _, ok := r.URL.Query()["connection"] if ok { return whoDeleteConnections(namespace, g, w, r) } if err := g.Delete(r.Context(), namespace, operator.CaseInsensitive{Key: entity.Name, Value: id}); err != nil { return err } return json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func whoDeleteConnections(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getID(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } connections, ok := r.URL.Query()["connection"] if !ok { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": "must provide connections to delete"}) } one := entity.One{Name: id} for _, connection := range connections { path := fmt.Sprintf("%s.%s", entity.Connections, connection) if err := g.Update(r.Context(), namespace, one.Query(), operator.Unset(path)); err != nil { return err } } return whoGet(namespace, g, w, r) } func whoPatch(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { id, err := getID(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } one := entity.One{} if err := json.NewDecoder(r.Body).Decode(&one); err != nil { return err } if one.Name == "" { http.Error(w, `{"error":"no name provided"}`, http.StatusBadRequest) return nil } relationship := one.Relationship one.Relationship = "" if err := g.Insert(r.Context(), namespace, one); err != nil && !strings.Contains(err.Error(), "ollision") { return err } one.Relationship = relationship if err := g.Update(r.Context(), namespace, entity.One{Name: id}, operator.Set{Key: fmt.Sprintf("%s.%s", entity.Connections, one.Name), Value: one.Peer()}); err != nil { return err } return whoGet(namespace, g, w, r) } func whoTrace(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error { ones, err := g.ListCaseInsensitive(r.Context(), namespace) if err != nil { return err } ones = sortOnes(ones, r) names := make([]string, len(ones)) for i := range ones { names[i] = ones[i].Name } enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(names) } func getCleanID(r *http.Request) (string, error) { id, err := getID(r) return sanitize(id), err } func getID(r *http.Request) (string, error) { id := r.URL.Query().Get("id") if id == "" { return "", errors.New("no id provided") } return id, nil } func sortOnesObject(ones []entity.One, r *http.Request) interface{} { ones = sortOnes(ones, r) m := orderedmap.New() for _, one := range ones { m.Set(one.Name, one) } return m } func sortOnes(ones []entity.One, r *http.Request) []entity.One { sorting := sanitize(r.URL.Query().Get(querySort)) if sorting == "" { sorting = entity.Modified } order := sanitize(r.URL.Query().Get(queryOrder)) if order == "" { order = "-1" } asc := order != "-1" sort.Slice(ones, func(i, j int) bool { if sorting == entity.Name { return (asc && ones[i].Name < ones[j].Name) || (!asc && ones[i].Name > ones[j].Name) } ib, _ := json.Marshal(ones[i]) jb, _ := json.Marshal(ones[j]) var im, jm bson.M json.Unmarshal(ib, &im) json.Unmarshal(jb, &jm) iv, _ := im[sorting] jv, _ := jm[sorting] is := fmt.Sprint(iv) js := fmt.Sprint(jv) return (asc && is < js) || (!asc && is > js) }) return ones } func sanitize(s string) string { re := regexp.MustCompile(`[^a-zA-Z0-9- _]`) s = re.ReplaceAllString(s, `.`) return s }