diff options
author | Steve Manuel <nilslice@gmail.com> | 2016-10-04 16:31:22 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2016-10-04 16:31:22 -0700 |
commit | 29c395635bbc5824f7864f774850d065b506c47d (patch) | |
tree | f1a3197bef08b4c14979d13da3264fdecd4489cf | |
parent | 100a66fd054dbc8c92cb2badd48becf18e13331f (diff) |
implementing support for file uploads, fixing some content issues with non-standard characters, removing base64 encoding of content
-rw-r--r-- | management/editor/elements.go | 44 | ||||
-rw-r--r-- | management/manager/manager.go | 6 | ||||
-rw-r--r-- | system/admin/admin.go | 1 | ||||
-rw-r--r-- | system/admin/server.go | 183 | ||||
-rw-r--r-- | system/admin/static/common/js/base64.min.js | 1 | ||||
-rw-r--r-- | system/api/server.go | 8 |
6 files changed, 216 insertions, 27 deletions
diff --git a/management/editor/elements.go b/management/editor/elements.go index 53f7146..418ee2e 100644 --- a/management/editor/elements.go +++ b/management/editor/elements.go @@ -3,6 +3,7 @@ package editor import ( "bytes" "fmt" + "html" "reflect" "strings" ) @@ -68,7 +69,7 @@ func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte { // create a hidden input to store the value from the struct val := valueFromStructField(fieldName, p).String() name := tagNameFromStructField(fieldName, p) - input := `<input type="hidden" name="` + name + `" class="richtext-value ` + fieldName + `" value="` + val + `"/>` + input := `<input type="hidden" name="` + name + `" class="richtext-value ` + fieldName + `" value="` + html.EscapeString(val) + `"/>` // build the dom tree for the entire richtext component iso = append(iso, domElement(div)...) @@ -93,26 +94,49 @@ func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte { ['para', ['ul', 'ol', 'paragraph']], ['height', ['height']], ['misc', ['codeview']] - ] + ], + // intercept file insertion, upload and insert img with new src + onImageUpload: function(files) { + var data = new FormData(); + data.append("file", files[0]); + $.ajax({ + data: data, + type: 'POST', + url: '/admin/edit/upload', + cache: false, + contentType: false, + processData: false, + success: function(resp) { + console.log(resp); + var img = document.createElement('img'); + img.setAttribute('src', resp.data[0].url); + console.log(img); + _editor.materialnote('insertNode', img); + }, + error: function(xhr, status, err) { + console.log(status, err); + } + }) + + } }); // inject content into editor if (hidden.val() !== "") { - console.log('content injected'); - _editor.code(Base64.decode(hidden.val())); + _editor.code(hidden.val()); } // update hidden input with encoded value on different events _editor.on('materialnote.change', function(e, content, $editable) { - console.log('content changed'); - hidden.val(Base64.encode(replaceBadChars(content))); + hidden.val(replaceBadChars(content)); }); _editor.on('materialnote.paste', function(e) { - console.log('content pasted'); - hidden.val(Base64.encode(replaceBadChars(_editor.code()))); + hidden.val(replaceBadChars(_editor.code())); }); + window._editor = _editor; + // bit of a hack to stop the editor buttons from causing a refresh when clicked $('.note-toolbar').find('button, i, a').on('click', function(e) { e.preventDefault(); }); }); @@ -230,7 +254,7 @@ func domElementSelfClose(e *element) []byte { e.viewBuf.Write([]byte(`<label class="active" for="` + strings.Join(strings.Split(e.label, " "), "-") + `">` + e.label + `</label>`)) } e.viewBuf.Write([]byte(`<` + e.TagName + ` value="`)) - e.viewBuf.Write([]byte(e.data + `" `)) + e.viewBuf.Write([]byte(html.EscapeString(e.data) + `" `)) for attr, value := range e.Attrs { e.viewBuf.Write([]byte(attr + `="` + value + `" `)) @@ -275,7 +299,7 @@ func domElement(e *element) []byte { e.viewBuf.Write([]byte(` name="` + e.Name + `"`)) e.viewBuf.Write([]byte(` >`)) - e.viewBuf.Write([]byte(e.data)) + e.viewBuf.Write([]byte(html.EscapeString(e.data))) e.viewBuf.Write([]byte(`</` + e.TagName + `>`)) e.viewBuf.Write([]byte(`</div>`)) diff --git a/management/manager/manager.go b/management/manager/manager.go index 7fd78ff..d69b810 100644 --- a/management/manager/manager.go +++ b/management/manager/manager.go @@ -10,14 +10,14 @@ import ( const managerHTML = ` <div class="card editor"> - <form method="post" action="/admin/edit"> + <form method="post" action="/admin/edit" enctype="multipart/form-data"> <input type="hidden" name="id" value="{{.ID}}"/> <input type="hidden" name="type" value="{{.Kind}}"/> {{ .Editor }} </form> <script> - // remove all bad chars from all inputs in the form - $('form input, form textarea').on('blur', function(e) { + // remove all bad chars from all inputs in the form, except file fields + $('form input:not([type=file]), form textarea').on('blur', function(e) { var val = e.target.value; e.target.value = replaceBadChars(val); }); diff --git a/system/admin/admin.go b/system/admin/admin.go index 3cfd770..410c9f4 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -14,7 +14,6 @@ const adminHTML = `<!doctype html> <head> <title>CMS</title> <script type="text/javascript" src="/admin/static/common/js/jquery-2.1.4.min.js"></script> - <script type="text/javascript" src="/admin/static/common/js/base64.min.js"></script> <script type="text/javascript" src="/admin/static/common/js/util.js"></script> <script type="text/javascript" src="/admin/static/dashboard/js/materialize.min.js"></script> <script type="text/javascript" src="/admin/static/editor/js/materialNote.js"></script> diff --git a/system/admin/server.go b/system/admin/server.go index 60b550e..b91635f 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -4,11 +4,14 @@ import ( "bytes" "encoding/json" "fmt" + "io" "log" "net/http" "os" "path/filepath" + "strconv" "strings" + "time" "github.com/nilslice/cms/content" "github.com/nilslice/cms/management/editor" @@ -16,7 +19,8 @@ import ( "github.com/nilslice/cms/system/db" ) -func init() { +// Run adds Handlers to default http listener for Admin +func Run() { http.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) { adminView, err := Admin(nil) if err != nil { @@ -31,12 +35,18 @@ func init() { http.HandleFunc("/admin/static/", func(res http.ResponseWriter, req *http.Request) { path := req.URL.Path + pathParts := strings.Split(path, "/")[1:] pwd, err := os.Getwd() if err != nil { log.Fatal("Coudln't get current directory to set static asset source.") } - http.ServeFile(res, req, filepath.Join(pwd, "system", path)) + filePathParts := make([]string, len(pathParts)+2, len(pathParts)+2) + filePathParts = append(filePathParts, pwd) + filePathParts = append(filePathParts, "system") + filePathParts = append(filePathParts, pathParts...) + + http.ServeFile(res, req, filepath.Join(filePathParts...)) }) http.HandleFunc("/admin/configure", func(res http.ResponseWriter, req *http.Request) { @@ -141,7 +151,7 @@ func init() { res.Write(adminView) case http.MethodPost: - err := req.ParseForm() + err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB if err != nil { fmt.Println(err) res.WriteHeader(http.StatusBadRequest) @@ -150,6 +160,34 @@ func init() { cid := req.FormValue("id") t := req.FormValue("type") + ts := req.FormValue("timestamp") + + // create a timestamp if one was not set + date := make(map[string]int) + if ts == "" { + now := time.Now() + date["year"] = now.Year() + date["month"] = int(now.Month()) + date["day"] = now.Day() + + // create timestamp format 'yyyy-mm-dd' and set in PostForm for + // db insertion + ts = fmt.Sprintf("%d-%02d-%02d", date["year"], date["month"], date["day"]) + req.PostForm.Set("timestamp", ts) + } + + urlPaths, err := storeFileUploads(req) + if err != nil { + fmt.Println(err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + for name, urlPath := range urlPaths { + req.PostForm.Add(name, urlPath) + } + + fmt.Println(req.PostForm) // check for any multi-value fields (ex. checkbox fields) // and correctly format for db storage. Essentially, we need @@ -187,9 +225,142 @@ func init() { http.Redirect(res, req, desURL, http.StatusFound) } }) + + http.HandleFunc("/admin/edit/upload", func(res http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + res.WriteHeader(http.StatusMethodNotAllowed) + return + } + + urlPaths, err := storeFileUploads(req) + if err != nil { + fmt.Println("Couldn't store file uploads.", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "application/json") + res.Write([]byte(`{"data": [{"url": "` + urlPaths["file"] + `"}]}`)) + }) + + // API path needs to be registered within server package so that it is handled + // even if the API server is not running. Otherwise, images/files uploaded + // through the editor will not load within the admin system. + http.HandleFunc("/api/uploads/", func(res http.ResponseWriter, req *http.Request) { + path := req.URL.Path + pathParts := strings.Split(path, "/")[2:] + + pwd, err := os.Getwd() + if err != nil { + log.Fatal("Coudln't get current directory to set static asset source.") + } + + filePathParts := make([]string, len(pathParts)+1, len(pathParts)+1) + filePathParts = append(filePathParts, pwd) + filePathParts = append(filePathParts, pathParts...) + + http.ServeFile(res, req, filepath.Join(filePathParts...)) + }) } -// Run starts the Admin system on the port provided -func Run(port string) { - http.ListenAndServe(":"+port, nil) +func storeFileUploads(req *http.Request) (map[string]string, error) { + err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + ts := req.FormValue("timestamp") + + // To use for FormValue name:urlPath + urlPaths := make(map[string]string) + + // get ts values individually to use as directory names when storing + // uploaded images + date := make(map[string]int) + if ts == "" { + now := time.Now() + date["year"] = now.Year() + date["month"] = int(now.Month()) + date["day"] = now.Day() + + // create timestamp format 'yyyy-mm-dd' and set in PostForm for + // db insertion + ts = fmt.Sprintf("%d-%02d-%02d", date["year"], date["month"], date["day"]) + req.PostForm.Set("timestamp", ts) + } else { + tsParts := strings.Split(ts, "-") + year, err := strconv.Atoi(tsParts[0]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + month, err := strconv.Atoi(tsParts[1]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + day, err := strconv.Atoi(tsParts[2]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + date["year"] = year + date["month"] = month + date["day"] = day + } + + // get or create upload directory to save files from request + pwd, err := os.Getwd() + if err != nil { + err := fmt.Errorf("Failed to locate current directory: %s", err) + return nil, err + } + + tsParts := strings.Split(ts, "-") + urlPathPrefix := "api" + uploadDirName := "uploads" + + uploadDir := filepath.Join(pwd, uploadDirName, tsParts[0], tsParts[1]) + err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm) + + // loop over all files and save them to disk + for name, fds := range req.MultipartForm.File { + filename := fds[0].Filename + src, err := fds[0].Open() + if err != nil { + err := fmt.Errorf("Couldn't open uploaded file: %s", err) + return nil, err + + } + defer src.Close() + + // check if file at path exists, if so, add timestamp to file + absPath := filepath.Join(uploadDir, filename) + + if _, err := os.Stat(absPath); !os.IsNotExist(err) { + fmt.Println(err, "file at", absPath, "exists") + filename = fmt.Sprintf("%d-%s", time.Now().Unix(), filename) + absPath = filepath.Join(uploadDir, filename) + } + + // save to disk (TODO: or check if S3 credentials exist, & save to cloud) + dst, err := os.Create(absPath) + if err != nil { + err := fmt.Errorf("Failed to create destination file for upload: %s", err) + return nil, err + } + + // copy file from src to dst on disk + if _, err = io.Copy(dst, src); err != nil { + err := fmt.Errorf("Failed to copy uploaded file to destination: %s", err) + return nil, err + } + + // add name:urlPath to req.PostForm to be inserted into db + urlPath := fmt.Sprintf("/%s/%s/%s/%s/%s", urlPathPrefix, uploadDirName, tsParts[0], tsParts[1], filename) + + urlPaths[name] = urlPath + } + + return urlPaths, nil } diff --git a/system/admin/static/common/js/base64.min.js b/system/admin/static/common/js/base64.min.js deleted file mode 100644 index 2ec7266..0000000 --- a/system/admin/static/common/js/base64.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(global){"use strict";var _Base64=global.Base64;var version="2.1.9";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=require("buffer").Buffer}catch(err){}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][-¿]","[à-ï][-¿]{2}","[ð-÷][-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/[\s\S]{1,4}/g,cb_decode)};var _decode=buffer?function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}})(this);
\ No newline at end of file diff --git a/system/api/server.go b/system/api/server.go index 5fa0bfc..5de10a2 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -10,7 +10,8 @@ import ( "github.com/nilslice/cms/system/db" ) -func init() { +// Run adds Handlers to default http listener for API +func Run() { http.HandleFunc("/api/types", func(res http.ResponseWriter, req *http.Request) { var types = []string{} for t := range content.Types { @@ -128,8 +129,3 @@ func wrapJSON(json []byte) []byte { return buf.Bytes() } - -// Run start the JSON API -func Run(port string) { - log.Fatal(http.ListenAndServe(":"+port, nil)) -} |