summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--management/editor/elements.go44
-rw-r--r--management/manager/manager.go6
-rw-r--r--system/admin/admin.go1
-rw-r--r--system/admin/server.go183
-rw-r--r--system/admin/static/common/js/base64.min.js1
-rw-r--r--system/api/server.go8
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))
-}