package api import ( "bytes" "compress/gzip" "encoding/json" "log" "net/http" "net/url" "strconv" "strings" "github.com/ponzu-cms/ponzu/system/api/analytics" "github.com/ponzu-cms/ponzu/system/db" "github.com/ponzu-cms/ponzu/system/item" ) // deprecating from API, but going to provide code here in case someone wants it func typesHandler(res http.ResponseWriter, req *http.Request) { var types = []string{} for t, fn := range item.Types { if !hide(fn(), res, req) { types = append(types, t) } } j, err := toJSON(types) if err != nil { res.WriteHeader(http.StatusInternalServerError) return } sendData(res, req, j) } func contentsHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() t := q.Get("type") if t == "" { res.WriteHeader(http.StatusBadRequest) return } it, ok := item.Types[t] if !ok { res.WriteHeader(http.StatusNotFound) return } if hide(it(), res, req) { return } count, err := strconv.Atoi(q.Get("count")) // int: determines number of posts to return (10 default, -1 is all) if err != nil { if q.Get("count") == "" { count = 10 } else { res.WriteHeader(http.StatusInternalServerError) return } } offset, err := strconv.Atoi(q.Get("offset")) // int: multiplier of count for pagination (0 default) if err != nil { if q.Get("offset") == "" { offset = 0 } else { res.WriteHeader(http.StatusInternalServerError) return } } order := strings.ToLower(q.Get("order")) // string: sort order of posts by timestamp ASC / DESC (DESC default) if order != "asc" { order = "desc" } opts := db.QueryOptions{ Count: count, Offset: offset, Order: order, } _, bb := db.Query(t+"__sorted", opts) var result = []json.RawMessage{} for i := range bb { result = append(result, bb[i]) } j, err := fmtJSON(result...) if err != nil { res.WriteHeader(http.StatusInternalServerError) return } sendData(res, req, j) } func contentHandler(res http.ResponseWriter, req *http.Request) { q := req.URL.Query() id := q.Get("id") t := q.Get("type") slug := q.Get("slug") if slug != "" { contentHandlerBySlug(res, req) return } if t == "" || id == "" { res.WriteHeader(http.StatusBadRequest) return } pt, ok := item.Types[t] if !ok { res.WriteHeader(http.StatusNotFound) return } if hide(pt(), res, req) { return } post, err := db.Content(t + ":" + id) if err != nil { res.WriteHeader(http.StatusInternalServerError) return } push(res, req, pt, post) j, err := fmtJSON(json.RawMessage(post)) if err != nil { res.WriteHeader(http.StatusInternalServerError) return } sendData(res, req, j) } func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { slug := req.URL.Query().Get("slug") if slug == "" { res.WriteHeader(http.StatusBadRequest) return } // lookup type:id by slug key in __contentIndex t, post, err := db.ContentBySlug(slug) if err != nil { log.Println("Error finding content by slug:", slug, err) res.WriteHeader(http.StatusInternalServerError) return } it, ok := item.Types[t] if !ok { res.WriteHeader(http.StatusBadRequest) return } if hide(it(), res, req) { return } push(res, req, it, post) j, err := fmtJSON(json.RawMessage(post)) if err != nil { res.WriteHeader(http.StatusInternalServerError) return } sendData(res, req, j) } func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { // check if should be hidden if h, ok := it.(item.Hideable); ok { err := h.Hide(res, req) if err == item.ErrAllowHiddenItem { return false } if err != nil { res.WriteHeader(http.StatusInternalServerError) return true } res.WriteHeader(http.StatusNotFound) return true } return false } func fmtJSON(data ...json.RawMessage) ([]byte, error) { var msg = []json.RawMessage{} for _, d := range data { msg = append(msg, d) } resp := map[string][]json.RawMessage{ "data": msg, } var buf = &bytes.Buffer{} enc := json.NewEncoder(buf) err := enc.Encode(resp) if err != nil { log.Println("Failed to encode data to JSON:", err) return nil, err } return buf.Bytes(), nil } func toJSON(data []string) ([]byte, error) { var buf = &bytes.Buffer{} enc := json.NewEncoder(buf) resp := map[string][]string{ "data": data, } err := enc.Encode(resp) if err != nil { log.Println("Failed to encode data to JSON:", err) return nil, err } return buf.Bytes(), nil } // sendData should be used any time you want to communicate // data back to a foreign client func sendData(res http.ResponseWriter, req *http.Request, data []byte) { res.Header().Set("Content-Type", "application/json") res.Header().Set("Vary", "Accept-Encoding") _, err := res.Write(data) if err != nil { log.Println("Error writing to response in sendData") } } // sendPreflight is used to respond to a cross-origin "OPTIONS" request func sendPreflight(res http.ResponseWriter) { res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") res.Header().Set("Access-Control-Allow-Origin", "*") res.WriteHeader(200) return } func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.ResponseWriter, bool) { if db.ConfigCache("cors_disabled").(bool) == true { // check origin matches config domain domain := db.ConfigCache("domain").(string) origin := req.Header.Get("Origin") u, err := url.Parse(origin) if err != nil { log.Println("Error parsing URL from request Origin header:", origin) return res, false } // hack to get dev environments to bypass cors since u.Host (below) will // be empty, based on Go's url.Parse function if domain == "localhost" { domain = "" } origin = u.Host // currently, this will check for exact match. will need feedback to // determine if subdomains should be allowed or allow multiple domains // in config if origin == domain { // apply limited CORS headers and return res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") res.Header().Set("Access-Control-Allow-Origin", domain) return res, true } // disallow request res.WriteHeader(http.StatusForbidden) return res, false } // apply full CORS headers and return res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") res.Header().Set("Access-Control-Allow-Origin", "*") return res, true } // CORS wraps a HandlerFunc to respond to OPTIONS requests properly func CORS(next http.HandlerFunc) http.HandlerFunc { return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res, cors := responseWithCORS(res, req) if !cors { return } if req.Method == http.MethodOptions { sendPreflight(res) return } next.ServeHTTP(res, req) })) } // Record wraps a HandlerFunc to record API requests for analytical purposes func Record(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { go analytics.Record(req) next.ServeHTTP(res, req) }) } // Gzip wraps a HandlerFunc to compress responses when possible func Gzip(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if db.ConfigCache("gzip_disabled").(bool) == true { next.ServeHTTP(res, req) return } // check if req header content-encoding supports gzip if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { // gzip response data res.Header().Set("Content-Encoding", "gzip") var gzres gzipResponseWriter if pusher, ok := res.(http.Pusher); ok { gzres = gzipResponseWriter{res, pusher, gzip.NewWriter(res)} } else { gzres = gzipResponseWriter{res, nil, gzip.NewWriter(res)} } next.ServeHTTP(gzres, req) return } next.ServeHTTP(res, req) }) } type gzipResponseWriter struct { http.ResponseWriter pusher http.Pusher gw *gzip.Writer } func (gzw gzipResponseWriter) Write(p []byte) (int, error) { defer gzw.gw.Close() return gzw.gw.Write(p) } func (gzw gzipResponseWriter) Push(target string, opts *http.PushOptions) error { if opts == nil { opts = &http.PushOptions{ Header: make(http.Header), } } opts.Header.Set("Accept-Encoding", "gzip") return gzw.pusher.Push(target, opts) }