From 914fafd8182aa073233709917566228cdea9ef2e Mon Sep 17 00:00:00 2001 From: Lafriakh Rachid Date: Sun, 21 Oct 2018 16:24:39 +0100 Subject: [PATCH] Initial commit --- LICENSE | 21 + README.md | 71 ++ config/config.go | 147 ++++ config/config_test.go | 25 + config/testdata/environments/development.yaml | 0 config/testdata/environments/production.yaml | 0 config/testdata/environments/test.yaml | 0 config/testdata/session.yaml | 1 + context.go | 58 ++ context_data.go | 11 + context_request.go | 93 +++ database/mongo/mongo.go | 50 ++ database/mysql/mysql.go | 44 + go.mod | 16 + go.sum | 29 + helpers/base64.go | 23 + helpers/bcrypt.go | 15 + helpers/byte.go | 70 ++ helpers/contains.go | 12 + helpers/convert.go | 72 ++ helpers/cookies.go | 18 + helpers/encrypt.go | 57 ++ helpers/encrypt_test.go | 27 + helpers/serialize.go | 25 + helpers/serialize_test.go | 32 + helpers/size.go | 28 + helpers/slices.go | 15 + helpers/slices_test.go | 13 + helpers/string.go | 58 ++ kira.go | 139 ++++ log.go | 56 ++ middleware.go | 23 + middlewares/csrf/csrf.go | 126 +++ middlewares/csrf/store.go | 20 + middlewares/example/example.go | 31 + middlewares/limitbody/limitbody.go | 34 + middlewares/logger/logger.go | 71 ++ middlewares/recover/recover.go | 80 ++ middlewares/requestid/request_id.go | 38 + middlewares/session/session.go | 60 ++ mysql.go | 42 + pagination/paginator.go | 193 +++++ router.go | 198 +++++ server.go | 70 ++ session.go | 26 + session/file_handler.go | 105 +++ session/handler.go | 10 + session/memory_handler.go | 53 ++ session/options.go | 75 ++ session/options_test.go | 16 + session/session.go | 133 +++ session/store.go | 199 +++++ session/util.go | 76 ++ upload/image.go | 31 + upload/mimes.go | 770 ++++++++++++++++++ upload/upload.go | 306 +++++++ validation.go | 30 + validation/patterns.go | 37 + validation/rules.go | 109 +++ validation/rules_errors.go | 12 + validation/validation.go | 63 ++ validation/validation_test.go | 68 ++ view.go | 182 +++++ 63 files changed, 4513 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/testdata/environments/development.yaml create mode 100644 config/testdata/environments/production.yaml create mode 100644 config/testdata/environments/test.yaml create mode 100644 config/testdata/session.yaml create mode 100644 context.go create mode 100644 context_data.go create mode 100644 context_request.go create mode 100644 database/mongo/mongo.go create mode 100644 database/mysql/mysql.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers/base64.go create mode 100644 helpers/bcrypt.go create mode 100644 helpers/byte.go create mode 100644 helpers/contains.go create mode 100644 helpers/convert.go create mode 100644 helpers/cookies.go create mode 100644 helpers/encrypt.go create mode 100644 helpers/encrypt_test.go create mode 100644 helpers/serialize.go create mode 100644 helpers/serialize_test.go create mode 100644 helpers/size.go create mode 100644 helpers/slices.go create mode 100644 helpers/slices_test.go create mode 100644 helpers/string.go create mode 100644 kira.go create mode 100644 log.go create mode 100644 middleware.go create mode 100644 middlewares/csrf/csrf.go create mode 100644 middlewares/csrf/store.go create mode 100644 middlewares/example/example.go create mode 100644 middlewares/limitbody/limitbody.go create mode 100644 middlewares/logger/logger.go create mode 100644 middlewares/recover/recover.go create mode 100644 middlewares/requestid/request_id.go create mode 100644 middlewares/session/session.go create mode 100644 mysql.go create mode 100644 pagination/paginator.go create mode 100644 router.go create mode 100644 server.go create mode 100644 session.go create mode 100644 session/file_handler.go create mode 100644 session/handler.go create mode 100644 session/memory_handler.go create mode 100644 session/options.go create mode 100644 session/options_test.go create mode 100644 session/session.go create mode 100644 session/store.go create mode 100644 session/util.go create mode 100644 upload/image.go create mode 100644 upload/mimes.go create mode 100644 upload/upload.go create mode 100644 validation.go create mode 100644 validation/patterns.go create mode 100644 validation/rules.go create mode 100644 validation/rules_errors.go create mode 100644 validation/validation.go create mode 100644 validation/validation_test.go create mode 100644 view.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34aa463 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Rachid Lafriakh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8346ffe --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Kira +Kira micro framework + +# Default example +``` +package main + +import ( + "github.com/Lafriakh/kira" +) + +func main() { + app := kira.New() + + app.GET("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello kira :)")) + }) + app.Run() +} + +``` +# Response +json +``` +app.JSON(w, data, 200) +``` +render template +``` +app.Render(w, data,"template/path") +``` +# Middleware +``` +// Log - log middleware +type Log struct{} + +func NewLogger() *Log { + return &Log{} +} + +// Handler - middleware handler +func (l *Log) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var now = time.Now() + + // logger message + log.Printf( + "%s\t%s\t%s", + r.Method, + r.RequestURI, + time.Since(now), + ) + + next.ServeHTTP(w, r) + }) +} + +// Pattern - middleware +func (l *Log) Pattern() []string { + return []string{"*"} +} + +// Name - middleware +func (l *Log) Name() string { + return "logger" +} + +``` +## TODO + +- [ ] Command-line interface (CLI) +- [ ] Live Reload diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b2abfa4 --- /dev/null +++ b/config/config.go @@ -0,0 +1,147 @@ +package config + +import ( + "bytes" + "errors" + "io/ioutil" + "path/filepath" + + "github.com/go-kira/kog" + + yaml "gopkg.in/yaml.v2" +) + +var errInvalidEnv = errors.New("invalid environment, use only: development, production, test") + +// DefaultPath to the configs folder. +var DefaultPath = "./config/" + +// DefaultVariablesPath enviroments path. +var DefaultVariablesPath = "./config/environments/" + +// Env type +type Env int + +const ( + // Development enviroment + Development Env = iota + // Production enviroment + Production + // Test enviroment + Test +) + +var envStrings = map[string]Env{ + "development": Development, + "production": Production, + "test": Test, +} +var envNames = [...]string{ + Production: "production", + Development: "development", + Test: "test", +} + +// to store all config files +var globalData map[string]interface{} + +// New return Config instance +func New(env string) map[string]interface{} { + // first check if the env supported. + if _, ok := envStrings[env]; !ok { + kog.Panic(errInvalidEnv) + } + + // load configs + configs := load(env) + + // return configs + if configs == nil { + // panic here + return globalData + } + // panic if there an error. + kog.Panic(configs) + + return globalData +} + +// Load config file data +func load(env string) error { + var buf bytes.Buffer + + // to store all config files + var files []string + + // first get all config files + configs, err := filepath.Glob(DefaultPath + "*.yaml") + if err != nil { + return err + } + files = append(files, configs...) + + // add env file to the config files. + // this will change any config value. + if env != "" { + files = append(files, DefaultVariablesPath+env+".yaml") + } + + // loop throw all + for _, config := range files { + // read file + data, err := ioutil.ReadFile(config) + if err != nil { + return err + } + + buf.Write(data) + buf.WriteString("\n") // add new line to the end of the file + } + + // Unmarshal toml data + yaml.Unmarshal(buf.Bytes(), &globalData) + + return nil +} + +// Get ... +func Get(key string) interface{} { + return globalData[key] +} + +// GetDefault return value by key, if the value empty set a default value. +func GetDefault(key string, def interface{}) interface{} { + val := globalData[key] + if val == nil { + return def + } + + return val +} + +// GetString - return config as string type. +func GetString(key string) string { + if globalData[key] == nil { + return "" + } + + return globalData[key].(string) +} + +// GetInt - return config as int type. +func GetInt(key string) int { + if globalData[key] == nil { + return 0 + } + + return globalData[key].(int) +} + +// GetBool - return config as int type. +func GetBool(key string) bool { + if globalData[key] == nil { + return false + } + + return globalData[key].(bool) +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..73069dd --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,25 @@ +package config + +import ( + "testing" +) + +func TestLoad(t *testing.T) { + DefaultPath = "./testdata" + DefaultVariablesPath = "./testdata/enviroments" + + New("production") + t.Fail() +} + +func TestGetDefault(t *testing.T) { + DefaultPath = "./testdata/" + DefaultVariablesPath = "./testdata/environments/" + + New("development") + def := GetDefault("SESSION_NAMEe", "default") + + if def != "default" { + t.Error("the value not 'default'") + } +} diff --git a/config/testdata/environments/development.yaml b/config/testdata/environments/development.yaml new file mode 100644 index 0000000..e69de29 diff --git a/config/testdata/environments/production.yaml b/config/testdata/environments/production.yaml new file mode 100644 index 0000000..e69de29 diff --git a/config/testdata/environments/test.yaml b/config/testdata/environments/test.yaml new file mode 100644 index 0000000..e69de29 diff --git a/config/testdata/session.yaml b/config/testdata/session.yaml new file mode 100644 index 0000000..203296f --- /dev/null +++ b/config/testdata/session.yaml @@ -0,0 +1 @@ +SESSION_NAME: kira \ No newline at end of file diff --git a/context.go b/context.go new file mode 100644 index 0000000..b224af2 --- /dev/null +++ b/context.go @@ -0,0 +1,58 @@ +package kira + +import ( + "fmt" + "net/http" + + "github.com/go-kira/kog" +) + +// Example: +// app.Get("/", func (ctx *kira.Context) { +// +// }) + +// ContextFunc - Type to define context function +type ContextFunc func(*Context) + +// Context ... +type Context struct { + request *http.Request + response http.ResponseWriter + Logger *kog.Logger + // The data assocaited with the request. + data map[string]interface{} + // Will hold the response status code. + statusCode int +} + +// NewContext - Create new instance of Context +func NewContext(res http.ResponseWriter, req *http.Request, logger *kog.Logger) *Context { + return &Context{ + request: req, + response: res, + Logger: logger, + data: make(map[string]interface{}), + } +} + +// Request - get the request +func (c *Context) Request() *http.Request { + return c.request +} + +// Response - get the response +func (c *Context) Response() http.ResponseWriter { + return c.response +} + +// Log - write the log +func (c *Context) Log() *kog.Logger { + return c.Logger +} + +// Error - stop the request with panic +func (c *Context) Error(msg ...interface{}) { + // Just panic and the recover will come to save us :) + panic(fmt.Sprint(msg...)) +} diff --git a/context_data.go b/context_data.go new file mode 100644 index 0000000..014801f --- /dev/null +++ b/context_data.go @@ -0,0 +1,11 @@ +package kira + +// SetData ... +func (c *Context) SetData(key string, data interface{}) { + c.data[key] = data +} + +// GetData ... +func (c *Context) GetData(key string) interface{} { + return c.data[key] +} diff --git a/context_request.go b/context_request.go new file mode 100644 index 0000000..5e4c313 --- /dev/null +++ b/context_request.go @@ -0,0 +1,93 @@ +package kira + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +// Query ... +func (c *Context) Query(param string) string { + return c.request.URL.Query().Get(param) +} + +// Var - get route variable. +func (c *Context) Var(variable string) string { + vars := mux.Vars(c.request) + + if val, ok := vars[variable]; ok { + return val + } + + return "" +} + +// Get request +func (a *App) Get(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"GET"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Post request +func (a *App) Post(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"POST"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Put request +func (a *App) Put(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"PUT"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Delete request +func (a *App) Delete(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"DELETE"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Head request +func (a *App) Head(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"HEAD"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Options request +func (a *App) Options(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"OPTIONS"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} + +// Patch request +func (a *App) Patch(pattern string, ctx ContextFunc) *Route { + route := &Route{Methods: []string{"PATCH"}, Pattern: pattern, HandlerFunc: func(w http.ResponseWriter, req *http.Request) { + ctx(NewContext(w, req, a.Log)) + }} + a.Routes = append(a.Routes, route) + + return route +} diff --git a/database/mongo/mongo.go b/database/mongo/mongo.go new file mode 100644 index 0000000..4efb398 --- /dev/null +++ b/database/mongo/mongo.go @@ -0,0 +1,50 @@ +package mongo + +import ( + "github.com/Lafriakh/log" + "github.com/go-kira/kon" + mgo "gopkg.in/mgo.v2" +) + +// Mongo - global mongo session. +var Mongo *mgo.Session + +// WithMongo - Open return new mongodb session +func WithMongo(configs *kon.Kon) *mgo.Session { + info := mgo.DialInfo{ + Addrs: []string{configs.GetString("DB_HOST")}, + Database: configs.GetString("DB_DATABASE"), + Username: configs.GetString("DB_USERNAME"), + Password: configs.GetString("DB_PASSWORD"), + } + + // Connect + session, err := mgo.DialWithInfo(&info) + if err != nil { + log.Panic(err) + } + // Optional. Switch the Session to a monotonic behavior. + session.SetMode(mgo.Monotonic, true) + + // append session to global variable. + Mongo = session + + // return mongo session. + return session +} + +// Insert - to insert data into collection. +func Insert(configs *kon.Kon, name string, docs ...interface{}) error { + err := Mongo.DB(configs.GetString("DB_DATABASE")).C(name).Insert(docs...) + return err +} + +// Database - return mongo database. +func Database(configs *kon.Kon, database string) *mgo.Database { + return Mongo.DB(configs.GetString("DB_DATABASE")) +} + +// C - return mongo collection. +func C(configs *kon.Kon, name string) *mgo.Collection { + return Mongo.DB(configs.GetString("DB_DATABASE")).C(name) +} diff --git a/database/mysql/mysql.go b/database/mysql/mysql.go new file mode 100644 index 0000000..ba6cead --- /dev/null +++ b/database/mysql/mysql.go @@ -0,0 +1,44 @@ +package kira + +import ( + "database/sql" + "fmt" + + _ "github.com/go-sql-driver/mysql" + + "github.com/Lafriakh/config" + "github.com/Lafriakh/log" +) + +// WithMysql - open an mysql connection. +func (a *App) WithMysql() { + var err error + + options := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?%s", + config.GetString("DB_USERNAME"), + config.GetString("DB_PASSWORD"), + config.GetDefault("DB_PROTOCOL", "tcp").(string), + config.GetString("DB_HOST"), + config.GetInt("DB_PORT"), + config.GetString("DB_DATABASE"), + config.GetDefault("DB_PARAMS", "charset=utf8&parseTime=true").(string), + ) + + // log.Debug(options) + + a.DB, err = sql.Open("mysql", options) + if err != nil { + log.Panic(err.Error()) + } + + // Open doesn't open a connection. Validate DSN data: + err = a.DB.Ping() + if err != nil { + log.Panic(err) + } +} + +// CloseMysql - for close mysql connection +func (a *App) CloseMysql() { + a.DB.Close() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d8fe034 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/Lafriakh/kira + +require ( + github.com/Lafriakh/config v0.0.0-20171223003722-dc734f141b64 + github.com/Lafriakh/log v0.0.0-20180527032428-2ebd3a914d94 + github.com/go-kira/kog v0.0.0-20180917154418-0a9eb8cd3237 + github.com/go-kira/kon v0.0.0-20181013202125-fb269f764192 + github.com/go-sql-driver/mysql v1.4.0 + github.com/google/uuid v1.0.0 + github.com/gorilla/mux v1.6.2 + golang.org/x/crypto v0.0.0-20181012144002-a92615f3c490 + golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 + gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v2 v2.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c3314e --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/Lafriakh/config v0.0.0-20171223003722-dc734f141b64 h1:vwP8f22KiiAVoydDOdi/2iAvUB6De62UbuUrnv4AjEE= +github.com/Lafriakh/config v0.0.0-20171223003722-dc734f141b64/go.mod h1:QG+JPifM/d3A7SEPexTtVMhdEOsI8+SgodF0HpXaorg= +github.com/Lafriakh/log v0.0.0-20180527032428-2ebd3a914d94 h1:V0Kjl3uKCY87RWW4hyGyVH2LwZ8SEqn9y1Uth/A2E90= +github.com/Lafriakh/log v0.0.0-20180527032428-2ebd3a914d94/go.mod h1:c6IliimJtYK78Ivh9MInHqaZf/+8RAz+cgCoz59Q98s= +github.com/go-kira/kog v0.0.0-20180917154418-0a9eb8cd3237 h1:5+TZMjl1sVLP6VhdhE7dES0RKkb4LF6txlADWHTUWEw= +github.com/go-kira/kog v0.0.0-20180917154418-0a9eb8cd3237/go.mod h1:zqGZJHidG6U0e34KJBAtij6d9AW+/dFynky0vyd/H60= +github.com/go-kira/kon v0.0.0-20181013165914-ab8946a5c597 h1:DxYN0WfRs7F+HEGMaiMXMSKWZNxnqTqNzvfFBt8ZjDE= +github.com/go-kira/kon v0.0.0-20181013165914-ab8946a5c597/go.mod h1:cwFD8HLwUTk0Jr6PZA1Gx2eylCmjVZV7JFs3EqKITkQ= +github.com/go-kira/kon v0.0.0-20181013202125-fb269f764192 h1:Z0spGbfYQrIPcOze6J6bJhtZ5Zpm5WihQzUBYVG/j/I= +github.com/go-kira/kon v0.0.0-20181013202125-fb269f764192/go.mod h1:cwFD8HLwUTk0Jr6PZA1Gx2eylCmjVZV7JFs3EqKITkQ= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +golang.org/x/crypto v0.0.0-20181012144002-a92615f3c490 h1:va0qYsIOza3Nlf2IncFyOql4/3XUq3vfge/Ad64bhlM= +golang.org/x/crypto v0.0.0-20181012144002-a92615f3c490/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/helpers/base64.go b/helpers/base64.go new file mode 100644 index 0000000..c2a9889 --- /dev/null +++ b/helpers/base64.go @@ -0,0 +1,23 @@ +package helpers + +import "encoding/base64" + +// EncodeBase64 - encode bytes to base64 string +func EncodeBase64(str []byte) string { + return base64.StdEncoding.EncodeToString(str) +} + +// DecodeBase64 - decode base64 string +func DecodeBase64(str string) ([]byte, error) { + return base64.StdEncoding.DecodeString(str) +} + +// EncodeURLBase64 - encode bytes to base64 string +func EncodeURLBase64(str []byte) string { + return base64.URLEncoding.EncodeToString(str) +} + +// DecodeURLBase64 - decode base64 string +func DecodeURLBase64(str string) ([]byte, error) { + return base64.URLEncoding.DecodeString(str) +} diff --git a/helpers/bcrypt.go b/helpers/bcrypt.go new file mode 100644 index 0000000..6991328 --- /dev/null +++ b/helpers/bcrypt.go @@ -0,0 +1,15 @@ +package helpers + +import "golang.org/x/crypto/bcrypt" + +// BcryptHash - generate hashed password. +func BcryptHash(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +// BcryptCompare - compares a bcrypt hashed password with its possible +// plaintext equivalent. Returns nil on success, or an error on failure. +func BcryptCompare(hash string, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} diff --git a/helpers/byte.go b/helpers/byte.go new file mode 100644 index 0000000..36dc8db --- /dev/null +++ b/helpers/byte.go @@ -0,0 +1,70 @@ +package helpers + +import ( + "crypto/rand" + "fmt" + "math" + "strconv" +) + +// RandomBytes - generate random bytes +func RandomBytes(length int) []byte { + b := make([]byte, length) + _, err := rand.Read(b) + if err != nil { + fmt.Println("error:", err) + return nil + } + return b +} + +// BytesFormat - format bytes to MB... +func BytesFormat(inputNum float64, precision int) string { + + RoundUp := func(input float64, places int) (newVal float64) { + var round float64 + pow := math.Pow(10, float64(places)) + digit := pow * input + round = math.Ceil(digit) + newVal = round / pow + return + } + + if precision <= 0 { + precision = 1 + } + + var unit string + var returnVal float64 + + if inputNum >= 1000000000000000000000000 { + returnVal = RoundUp((inputNum / 1208925819614629174706176), precision) + unit = " YB" // yottabyte + } else if inputNum >= 1000000000000000000000 { + returnVal = RoundUp((inputNum / 1180591620717411303424), precision) + unit = " ZB" // zettabyte + } else if inputNum >= 10000000000000000000 { + returnVal = RoundUp((inputNum / 1152921504606846976), precision) + unit = " EB" // exabyte + } else if inputNum >= 1000000000000000 { + returnVal = RoundUp((inputNum / 1125899906842624), precision) + unit = " PB" // petabyte + } else if inputNum >= 1000000000000 { + returnVal = RoundUp((inputNum / 1099511627776), precision) + unit = " TB" // terrabyte + } else if inputNum >= 1000000000 { + returnVal = RoundUp((inputNum / 1073741824), precision) + unit = " GB" // gigabyte + } else if inputNum >= 1000000 { + returnVal = RoundUp((inputNum / 1048576), precision) + unit = " MB" // megabyte + } else if inputNum >= 1000 { + returnVal = RoundUp((inputNum / 1024), precision) + unit = " KB" // kilobyte + } else { + returnVal = inputNum + unit = " bytes" // byte + } + + return strconv.FormatFloat(returnVal, 'f', precision, 64) + unit +} diff --git a/helpers/contains.go b/helpers/contains.go new file mode 100644 index 0000000..5314b9f --- /dev/null +++ b/helpers/contains.go @@ -0,0 +1,12 @@ +package helpers + +// Contains - check if the slice of strings containt the given string +func Contains(vals []string, s string) bool { + for _, v := range vals { + if v == s { + return true + } + } + + return false +} diff --git a/helpers/convert.go b/helpers/convert.go new file mode 100644 index 0000000..f5d133a --- /dev/null +++ b/helpers/convert.go @@ -0,0 +1,72 @@ +package helpers + +import ( + "log" + "strconv" +) + +// ConvertToString convert any data type to specific type +func ConvertToString(value interface{}) string { + switch value.(type) { + case bool: + return strconv.FormatBool(value.(bool)) + case int: + return strconv.Itoa(value.(int)) + case float64: + return strconv.FormatFloat(value.(float64), 'f', -1, 64) + case nil: + return "" + default: + return value.(string) + } +} + +// ConvertToInteger convert any data type to specific type +func ConvertToInteger(value interface{}) int { + switch value.(type) { + case string: + if value.(string) == "" { + return 0 + } + + integer, err := strconv.Atoi(value.(string)) + if err != nil { + log.Fatal(err) + } + return integer + case nil: + return 0 + default: + return value.(int) + } +} + +// ConvertToFloat64 convert any data type to float64 +func ConvertToFloat64(value interface{}) float64 { + switch value.(type) { + case string: + float, err := strconv.ParseFloat(value.(string), 64) + if err != nil { + log.Fatal(err) + } + return float + case int: + return float64(value.(float64)) + default: + return value.(float64) + } +} + +// ConvertToBool convert any data type to bool +func ConvertToBool(value interface{}) bool { + switch value.(type) { + case string: + bool, err := strconv.ParseBool(value.(string)) + if err != nil { + log.Fatal(err) + } + return bool + default: + return value.(bool) + } +} diff --git a/helpers/cookies.go b/helpers/cookies.go new file mode 100644 index 0000000..63bbd77 --- /dev/null +++ b/helpers/cookies.go @@ -0,0 +1,18 @@ +package helpers + +import "net/http" + +// GetCookie - get the cookies from the request. +func GetCookie(name string, request *http.Request) (string, error) { + cookie, err := request.Cookie(name) + if err != nil { + return "", err + } + + return cookie.Value, nil +} + +// SetCookie - set cookies to the response. +func SetCookie(cookie http.Cookie, response http.ResponseWriter) { + http.SetCookie(response, &cookie) +} diff --git a/helpers/encrypt.go b/helpers/encrypt.go new file mode 100644 index 0000000..0973715 --- /dev/null +++ b/helpers/encrypt.go @@ -0,0 +1,57 @@ +package helpers + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of +// the data and provides a check that it hasn't been altered. Output takes the +// form nonce|ciphertext|tag where '|' indicates concatenation. +func Encrypt(plaintext []byte, key []byte) (ciphertext []byte, err error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of +// the data and provides a check that it hasn't been altered. Expects input +// form nonce|ciphertext|tag where '|' indicates concatenation. +func Decrypt(ciphertext []byte, key []byte) (plaintext []byte, err error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("malformed ciphertext") + } + + return gcm.Open(nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) +} diff --git a/helpers/encrypt_test.go b/helpers/encrypt_test.go new file mode 100644 index 0000000..7545ce4 --- /dev/null +++ b/helpers/encrypt_test.go @@ -0,0 +1,27 @@ +package helpers + +import ( + "encoding/hex" + "fmt" +) + +func ExampleEncrypt() { + key := []byte("9d8b23c2529ced916abaf60599fb3110") + text := []byte("Testing encrypt func") + + doTest, _ := Encrypt(text, key) + + fmt.Printf("%x\n", doTest) +} + +func ExampleDecrypt() { + key := []byte("9d8b23c2529ced916abaf60599fb3110") + text, _ := hex.DecodeString("537235e0ba1c4551c1787ab68ceb4bc3c6e738f0e3b3e8656322932e2a56969b4bcf5afb53e07d082b5cd61e8a451433") + + doTest, _ := Decrypt(text, key) + + fmt.Printf("%s\n", doTest) + // fmt.Println(err) + + // Output: Testing encrypt func +} diff --git a/helpers/serialize.go b/helpers/serialize.go new file mode 100644 index 0000000..f5606f7 --- /dev/null +++ b/helpers/serialize.go @@ -0,0 +1,25 @@ +package helpers + +import ( + "bytes" + "encoding/gob" +) + +// Serialize encodes a value using gob. +func Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return err + } + return nil +} diff --git a/helpers/serialize_test.go b/helpers/serialize_test.go new file mode 100644 index 0000000..415375a --- /dev/null +++ b/helpers/serialize_test.go @@ -0,0 +1,32 @@ +package helpers + +import ( + "encoding/hex" + "fmt" + "testing" +) + +func TestSerialize(t *testing.T) { + key := []byte("9d8b23c2529ced916abaf60599fb3110") + text := []byte("Testing encrypt func") + + doTest, _ := Encrypt(text, key) + + serialize, _ := Serialize(doTest) + + fmt.Println(string(serialize)) + + t.Fail() +} + +func ExampleDeserialize() { + key := []byte("9d8b23c2529ced916abaf60599fb3110") + text, _ := hex.DecodeString("537235e0ba1c4551c1787ab68ceb4bc3c6e738f0e3b3e8656322932e2a56969b4bcf5afb53e07d082b5cd61e8a451433") + + doTest, _ := Decrypt(text, key) + + fmt.Printf("%s\n", doTest) + // fmt.Println(err) + + // Output: Testing encrypt func +} diff --git a/helpers/size.go b/helpers/size.go new file mode 100644 index 0000000..1c25128 --- /dev/null +++ b/helpers/size.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "unicode/utf8" +) + +// Size return size of string or float64 or float32 or slice +func Size(value interface{}) float64 { + switch value.(type) { + // convert string value to integer + case string: + return float64(utf8.RuneCountInString(value.(string))) + case float32: + return float64(value.(float32)) + case int: + return float64(value.(int)) + case []string: + return float64(len(value.([]string))) + case []int: + return float64(len(value.([]int))) + case []float32: + return float64(len(value.([]float32))) + case []float64: + return float64(len(value.([]float64))) + default: + return value.(float64) + } +} diff --git a/helpers/slices.go b/helpers/slices.go new file mode 100644 index 0000000..d89bc40 --- /dev/null +++ b/helpers/slices.go @@ -0,0 +1,15 @@ +package helpers + +// Diffrence - returns the values in slice1 that are not present in any of the other slices. +func Diffrence(slice []string, slice2 []string) []string { + var result []string + + // range over slice + for _, v := range slice { + if !Contains(slice2, v) { + result = append(result, v) + } + } + + return result +} diff --git a/helpers/slices_test.go b/helpers/slices_test.go new file mode 100644 index 0000000..0f4073c --- /dev/null +++ b/helpers/slices_test.go @@ -0,0 +1,13 @@ +package helpers + +import "fmt" + +func ExampleDiffrence() { + slice1 := []string{"Rachid", "Lafriakh", "foo", "bar"} + slice2 := []string{"Rachid", "Lafriakh"} + + diff := Diffrence(slice2, slice1) + + fmt.Println(diff) + // Output: +} diff --git a/helpers/string.go b/helpers/string.go new file mode 100644 index 0000000..e6c7b75 --- /dev/null +++ b/helpers/string.go @@ -0,0 +1,58 @@ +package helpers + +import ( + "strings" +) + +// Before Get substring before a string. +func Before(value string, a string) string { + // Get substring before a string. + pos := strings.Index(value, a) + if pos == -1 { + return "" + } + return value[0:pos] +} + +// After Get substring after a string. +func After(value string, a string) string { + // Get substring after a string. + pos := strings.LastIndex(value, a) + if pos == -1 { + return "" + } + adjustedPos := pos + len(a) + if adjustedPos >= len(value) { + return "" + } + return value[adjustedPos:len(value)] +} + +// Between Get substring between two strings. +func Between(value string, a string, b string) string { + // Get substring between two strings. + posFirst := strings.Index(value, a) + if posFirst == -1 { + return "" + } + posLast := strings.Index(value, b) + if posLast == -1 { + return "" + } + posFirstAdjusted := posFirst + len(a) + if posFirstAdjusted >= posLast { + return "" + } + return value[posFirstAdjusted:posLast] +} + +// RandomString return random string by lenght +func RandomString(length int) string { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + bytes := RandomBytes(length) + + for i, b := range bytes { + bytes[i] = letters[b%byte(len(letters))] + } + return string(bytes) +} diff --git a/kira.go b/kira.go new file mode 100644 index 0000000..5ee8af5 --- /dev/null +++ b/kira.go @@ -0,0 +1,139 @@ +package kira + +import ( + "database/sql" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/Lafriakh/kira/session" + "github.com/Lafriakh/kira/validation" + "github.com/go-kira/kog" + "github.com/go-kira/kon" // "github.com/Lafriakh/env" + "github.com/gorilla/mux" +) + +var hero = ` + __ __ _ + / //_/ (_) ____ ___ _ + / ,< / / / __// _ / +/_/|_| /_/ /_/ \_,_/ +` + +// some bytes :) +const ( + KB = 1 << 10 + MB = 1 << 20 + GB = 1 << 30 +) + +// you can customize this before init. +var ( + PathStore = "./storage" + PathApp = PathStore + "/app" + PathResource = "./resources" + PathView = PathResource + "/views" + PathSass = PathResource + "/sass" + PathJS = PathResource + "/javascript" + PathSession = PathStore + "/framework/sessions" + PathLogs = PathStore + "/framework/logs" +) + +// App hold the framework options +type App struct { + Routes []*Route + Middlewares []Middleware + Router *mux.Router + View View + Validation *validation.Validation + Session *session.Session + Log *kog.Logger + Configs *kon.Kon + Env string + DB *sql.DB + + isTLS bool +} + +// New init the framework +func New() *App { + // initialization... + app := &App{} + + // kira environment + app.Env = getEnv() + + // configs + app.Configs = getConfig() + + // logger + app.Log = setupLogger(app.Configs) + + // init view with app instance + app.View.Data = make(map[string]interface{}) + app.View.App = app + + // validation + app.Validation = validation.New() + + // session + app.Session = setupSession(app.Configs) + + // define a Router + app.Router = mux.NewRouter().StrictSlash(true) + + // return App instance + return app +} + +// Run the framework +func (a *App) Run() *App { + fmt.Printf("%v", hero) + + // parse routes & middlewares + a.NewRouter() + + // validate if the server need tls connection. + if !a.isTLS { + // Start the server + a.StartServer() + } else { + // TLS + a.StartTLSServer() + } + + // return App instance + return a +} + +// getEnv for set the framework environment. +func getEnv() string { + // Get the environment from .kira_env file. + if _, err := os.Stat("./.kira_env"); !os.IsNotExist(err) { + // path/to/whatever exists + kiraEnv, err := ioutil.ReadFile("./.kira_env") + if err != nil { + kog.Panic(err) + } + return strings.TrimSpace(string(kiraEnv)) + } + + // Get the environment from system variable + osEnv := os.Getenv("KIRA_ENV") + if osEnv == "" { + return "development" + } + return osEnv +} + +func getConfig() *kon.Kon { + var files = []string{"./config/application.kon"} + var env = fmt.Sprintf("./config/environments/%s.kon", getEnv()) + + if _, err := os.Stat(env); !os.IsNotExist(err) { + files = append(files, env) + } + + return kon.NewFromFile(files...) +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..cf10a99 --- /dev/null +++ b/log.go @@ -0,0 +1,56 @@ +package kira + +import ( + "io" + "os" + + "github.com/go-kira/kog" + "github.com/go-kira/kon" + lumberjack "gopkg.in/natefinch/lumberjack.v2" +) + +func setupLogger(config *kon.Kon) *kog.Logger { + logger := kog.New(setupWriter(config), setupFormatter()) + logger.SetLevel(kog.LevelStrings[config.GetString("LOG_LEVEL")]) + + return logger +} + +func setupWriter(config *kon.Kon) io.Writer { + switch config.GetString("LOG") { + case "stderr": + return os.Stderr + case "stdin": + return os.Stdin + case "stdout": + return os.Stdout + case "file": + return logToFile(config) + } + + return os.Stderr +} + +// setupFormatter to setup the logger formatter. +func setupFormatter() kog.Formatter { + // TODO + // - Add color formatter + return kog.NewDefaultFormatter() +} + +// LoggerToFile - make evrey log in log file +// append log to this destination file: storage/logs/year/month/day/logs.log +func logToFile(config *kon.Kon) io.Writer { + // TODO + // Rotate file log + // set a max size of log file + // when the file rish the limit, create new one. + + return &lumberjack.Logger{ + Filename: config.GetString("LOG_FILE"), + MaxSize: config.GetInt("LOG_FILE_MAX_SIZE"), + MaxBackups: config.GetInt("LOG_FILE_MAX_BACKUPS"), + MaxAge: config.GetInt("LOG_FILE_MAX_AGE"), + Compress: config.GetBool("LOG_FILE_MAX_COMPRESS"), + } +} diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..7ea78d8 --- /dev/null +++ b/middleware.go @@ -0,0 +1,23 @@ +package kira + +import ( + "net/http" +) + +// Middleware interface +type Middleware interface { + Handler(next http.Handler) http.Handler +} + +// UseMiddlewares - assign many middlewares +func (a *App) UseMiddlewares(m []Middleware) { + for _, middlware := range m { + a.Middlewares = append(a.Middlewares, middlware) + } + // a.Middlewares = m +} + +// UseMiddleware - add the middleware +func (a *App) UseMiddleware(m Middleware) { + a.Middlewares = append(a.Middlewares, m) +} diff --git a/middlewares/csrf/csrf.go b/middlewares/csrf/csrf.go new file mode 100644 index 0000000..0e21d36 --- /dev/null +++ b/middlewares/csrf/csrf.go @@ -0,0 +1,126 @@ +package csrf + +import ( + "context" + "errors" + "log" + "net/http" + + "github.com/Lafriakh/kira" + "github.com/Lafriakh/kira/helpers" + "github.com/Lafriakh/kira/session" + "github.com/go-kira/kon" + "golang.org/x/net/xsrftoken" +) + +var ( + safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"} +) + +// errors +var ( + // ErrNoToken is returned if no CSRF token is supplied in the request. + ErrNoToken = errors.New("CSRF token not found in request") + // ErrBadToken is returned if the CSRF token in the request does not match + // the token in the session, or is otherwise malformed. + ErrBadToken = errors.New("CSRF token invalid") +) + +// CSRF Middelware +type CSRF struct { + *kira.App +} + +// NewCSRF ... +func NewCSRF(app *kira.App) *CSRF { + return &CSRF{app} +} + +// Handler ... +func (c *CSRF) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // check if there a token, if not generate new one. + if !c.App.Session.Has("_token") { + c.RegenerateToken(c.App.Configs) + } + + // context + ctx := context.WithValue(r.Context(), "csrf", c.Token()) + + // only on POST... + if err := tokensMatch(c.App.Configs, r, c.App.Session.Get("_token").(string)); err == nil || isReading(r) { + // addCookieToResponse + c.addCookieToResponse(c.App.Configs, w, r) + } else { + log.Panic(err) + } + + // Go to the next request + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// Determine if the HTTP request uses a ‘read’ verb. +func isReading(r *http.Request) bool { + if helpers.Contains(safeMethods, r.Method) { + return true + } + return false +} + +// Determine if the session and input CSRF tokens match. +func tokensMatch(config *kon.Kon, r *http.Request, sessionToken string) error { + // get the token from the request. + getToken := getTokenFromRequest(config, r) + if getToken == "" { + return ErrNoToken + } + + // decode the token from session. + token, err := helpers.DecodeURLBase64(getToken) + if err != nil { + return err + } + + // validate if the token from the request equal to the token from the session. + if string(token) == sessionToken { + // validate the session timing + if xsrftoken.Valid(string(token), config.GetString("APP_KEY"), "", "") { + return nil + } + } + + return ErrBadToken +} + +func getTokenFromRequest(config *kon.Kon, r *http.Request) string { + // 1. Check the HTTP header first. + token := r.Header.Get(config.GetString("CSRF_HEADER_NAME")) + + // 2. Fall back to the POST (form) value. + if token == "" { + token = r.PostFormValue(config.GetString("CSRF_FIELD_NAME")) + } + + // 3. Finally, fall back to the multipart form (if set). + if token == "" && r.MultipartForm != nil { + vals := r.MultipartForm.Value[config.GetString("CSRF_FIELD_NAME")] + + if len(vals) > 0 { + token = vals[0] + } + } + + return token +} + +func (c *CSRF) addCookieToResponse(config *kon.Kon, w http.ResponseWriter, r *http.Request) { + // set cookie to the response + http.SetCookie(w, session.NewCookie( + config, + config.GetString("CSRF_COOKIE_NAME"), + helpers.EncodeBase64([]byte(c.App.Session.Get("_token").(string))), + c.App.Session.Options, + )) + +} diff --git a/middlewares/csrf/store.go b/middlewares/csrf/store.go new file mode 100644 index 0000000..2c2f165 --- /dev/null +++ b/middlewares/csrf/store.go @@ -0,0 +1,20 @@ +package csrf + +import ( + "github.com/Lafriakh/kira/helpers" + "github.com/go-kira/kon" + "golang.org/x/net/xsrftoken" +) + +// Token - Get the CSRF token value. +func (c *CSRF) Token() string { + return helpers.EncodeURLBase64([]byte(c.App.Session.Get("_token").(string))) +} + +// RegenerateToken - Regenerate the CSRF token value. +func (c *CSRF) RegenerateToken(config *kon.Kon) { + csrf := xsrftoken.Generate(config.GetString("APP_KEY"), "", "") + + // add the token to the session + c.App.Session.Put("_token", csrf) +} diff --git a/middlewares/example/example.go b/middlewares/example/example.go new file mode 100644 index 0000000..85aba33 --- /dev/null +++ b/middlewares/example/example.go @@ -0,0 +1,31 @@ +package example + +import ( + "github.com/Lafriakh/kira" +) + +// Example - kira middleware example. +type Example struct { + *kira.App +} + +// NewExample - a new instance of Example +func NewExample(app *kira.App) *Example { + return &Example{app} +} + +// func (m *Example) ServeHTTP(next *http.Handler) *http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// // Before request +// next.ServeHTTP(w, r) +// // After request +// }) +// } + +// func (m *Example) ServeHTTP(next *kira.Context) *http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// // Before request +// next.ServeHTTP(w, r) +// // After request +// }) +// } diff --git a/middlewares/limitbody/limitbody.go b/middlewares/limitbody/limitbody.go new file mode 100644 index 0000000..3f51e50 --- /dev/null +++ b/middlewares/limitbody/limitbody.go @@ -0,0 +1,34 @@ +package limitbody + +import ( + "net/http" + + "github.com/Lafriakh/kira" +) + +// MB - one MB. +const MB = 1 << 20 + +// Limitbody - Middleware. +type Limitbody struct { + *kira.App +} + +// Newlimitbody - return Limitbody instance +func Newlimitbody(app *kira.App) *Limitbody { + return &Limitbody{app} +} + +// Handler - middelware handler +func (l *Limitbody) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.ContentLength > l.App.Configs.GetInt64("SERVER_BODY_LIMIT")*MB { + http.Error(w, "Request too large", http.StatusExpectationFailed) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, l.App.Configs.GetInt64("SERVER_BODY_LIMIT")*MB) + + next.ServeHTTP(w, r) + }) +} diff --git a/middlewares/logger/logger.go b/middlewares/logger/logger.go new file mode 100644 index 0000000..4c9d871 --- /dev/null +++ b/middlewares/logger/logger.go @@ -0,0 +1,71 @@ +package logger + +import ( + "net/http" + "time" + + "github.com/Lafriakh/kira" + "github.com/go-kira/kog" +) + +const basePath = "storage/logs/" + +// colors +// var ( +// yellow = color.New(color.FgYellow).SprintFunc() +// red = color.New(color.FgRed).SprintFunc() +// green = color.New(color.FgGreen).SprintFunc() +// blue = color.New(color.FgBlue).SprintFunc() +// cyan = color.New(color.FgCyan).SprintFunc() +// ) + +// Log - log middleware +type Log struct { + *kira.App +} + +// NewLogger ... +func NewLogger(app *kira.App) *Log { + return &Log{app} +} + +// Handler - middleware handler +func (l *Log) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Start time + var now = time.Now() + // Store the status code + statusRecorder := &statusRecorder{w, http.StatusOK} + + // Run the request + next.ServeHTTP(statusRecorder, r) + + // logger message + // [INFO] [5b15c100-fdd3-482f-ad0c-037d4159d066] 2018/10/17 00:17:26 | [500] GET /seasons?page=1 63.803024ms + l.App.Log.Infof("[%s] %s [%d] %s %s %v", + // request id + r.Context().Value(l.App.Configs.GetString("SERVER_REQUEST_ID")).(string), + // time + kog.FormatTime(time.Now()), + // status code + statusRecorder.statusCode, + // method + r.Method, + // request path + r.RequestURI, + // request duration + time.Since(now), + ) + }) +} + +type statusRecorder struct { + http.ResponseWriter + statusCode int +} + +// WriteHeader - store the header to use it later. +func (r *statusRecorder) WriteHeader(code int) { + r.statusCode = code + r.ResponseWriter.WriteHeader(code) +} diff --git a/middlewares/recover/recover.go b/middlewares/recover/recover.go new file mode 100644 index 0000000..dbe45a6 --- /dev/null +++ b/middlewares/recover/recover.go @@ -0,0 +1,80 @@ +package recover + +import ( + "net/http" + "runtime" + "time" + + "github.com/Lafriakh/kira" + "github.com/go-kira/kog" +) + +// Recover - Middleware +type Recover struct { + *kira.App +} + +// NewRecover - return recover instance +func NewRecover(app *kira.App) *Recover { + return &Recover{app} +} + +// Handler - middelware handler +func (rc *Recover) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + defer func() { + r := recover() + // We have a problem here + if r != nil { + requestID := request.Context().Value(rc.App.Configs.GetString("SERVER_REQUEST_ID")).(string) + + // log the error + rc.App.Log.Errorf("[%s] %s | %s", requestID, kog.FormatTime(time.Now()), r) + + // write header + w.WriteHeader(http.StatusInternalServerError) + + // if the debug mode is enabled, add the stack to the error view + if rc.App.Configs.GetBool("DEBUG") { + rc.View.Data["message"] = r + rc.View.Data["frames"] = getFrames(100) + rc.View.Render(w, request, "errors/debug") + return + } + + // display error page + rc.View.Render(w, request, "errors/500") + return + } + }() + + next.ServeHTTP(w, request) + return + }) +} + +func getFrames(limit int) (framesSlice []runtime.Frame) { + // Ask runtime.Callers for up to 10 pcs, including runtime.Callers itself. + pc := make([]uintptr, limit) + n := runtime.Callers(0, pc) + if n == 0 { + // No pcs available. Stop now. + // This can happen if the first argument to runtime.Callers is large. + return + } + + pc = pc[:n] // pass only valid pcs to runtime.CallersFrames + frames := runtime.CallersFrames(pc) + + // Loop to get frames. + // A fixed number of pcs can expand to an indefinite number of Frames. + for { + frame, more := frames.Next() + framesSlice = append(framesSlice, frame) + if !more { + break + } + } + + return framesSlice +} diff --git a/middlewares/requestid/request_id.go b/middlewares/requestid/request_id.go new file mode 100644 index 0000000..f0a7f9e --- /dev/null +++ b/middlewares/requestid/request_id.go @@ -0,0 +1,38 @@ +package requestid + +import ( + "context" + "net/http" + + "github.com/go-kira/kon" + "github.com/google/uuid" +) + +// RequestID struct +type RequestID struct{ HeaderName string } + +// NewRequestID - new instance of RequestID. +func NewRequestID(config *kon.Kon) *RequestID { + return &RequestID{ + HeaderName: config.GetString("SERVER_REQUEST_ID"), + } +} + +// Handler - middleware handler +func (rq *RequestID) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // request id context + ctx := context.WithValue(r.Context(), rq.HeaderName, rq.random()) + + // set header + w.Header().Set(rq.HeaderName, rq.random()) + + // next handler + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// random return random string for request id +func (rq *RequestID) random() string { + return uuid.New().String() +} diff --git a/middlewares/session/session.go b/middlewares/session/session.go new file mode 100644 index 0000000..50d6039 --- /dev/null +++ b/middlewares/session/session.go @@ -0,0 +1,60 @@ +package session + +import ( + "net/http" + + "github.com/Lafriakh/kira" + "github.com/Lafriakh/kira/helpers" + "github.com/Lafriakh/kira/session" +) + +// Middleware - Middleware +type Middleware struct { + *kira.App +} + +// NewMiddleware - return session middlware instance +func NewMiddleware(app *kira.App) *Middleware { + // start session GC + app.Session.StartGC(app.Configs) + + // return session middleware + return &Middleware{app} +} + +// Handler - middelware handler +func (m *Middleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + store := m.startSession(request) + + // save cookies + http.SetCookie(response, session.NewCookie( + m.App.Configs, + m.Session.Options.Name, + helpers.EncodeBase64([]byte(store.GetID())), + m.Session.Options, + )) + + next.ServeHTTP(response, request) + + // save the session + store.Save() + }) +} + +func (m *Middleware) startSession(req *http.Request) *session.Store { + session := m.getSession(req) + session.Start() + return session +} + +// getSession get the session from the store +func (m *Middleware) getSession(request *http.Request) *session.Store { + store := m.Session.Store + + // read the session id from the cookie + cookie, _ := helpers.GetCookie(m.Session.Options.Name, request) + store.SetID(session.ParseID(cookie)) + + return store +} diff --git a/mysql.go b/mysql.go new file mode 100644 index 0000000..0d50916 --- /dev/null +++ b/mysql.go @@ -0,0 +1,42 @@ +package kira + +import ( + "database/sql" + "fmt" + + "github.com/Lafriakh/log" + _ "github.com/go-sql-driver/mysql" +) + +// WithMysql - open an mysql connection. +func (a *App) WithMysql() { + var err error + + options := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?%s", + a.Configs.GetString("DB_USERNAME"), + a.Configs.GetString("DB_PASSWORD"), + a.Configs.GetDefault("DB_PROTOCOL", "tcp").(string), + a.Configs.GetString("DB_HOST"), + a.Configs.GetInt("DB_PORT"), + a.Configs.GetString("DB_DATABASE"), + a.Configs.GetDefault("DB_PARAMS", "charset=utf8&parseTime=true").(string), + ) + + // log.Debug(options) + + a.DB, err = sql.Open("mysql", options) + if err != nil { + log.Panic(err.Error()) + } + + // Open doesn't open a connection. Validate DSN data: + err = a.DB.Ping() + if err != nil { + log.Panic(err) + } +} + +// CloseMysql - for close mysql connection +func (a *App) CloseMysql() { + a.DB.Close() +} diff --git a/pagination/paginator.go b/pagination/paginator.go new file mode 100644 index 0000000..64dff1a --- /dev/null +++ b/pagination/paginator.go @@ -0,0 +1,193 @@ +package pagination + +import ( + "bytes" + "html/template" + "log" + "math" + + "github.com/go-kira/kon" + + "github.com/Lafriakh/kira/helpers" +) + +const defaultTemplate = "default" + +// Pagination ... +type Pagination struct { + total, perPage, currentPage int + url string + + numPages int + maxPagesToShow int + lastPage float64 + template string + + configs *kon.Kon +} + +// Page ... +type Page struct { + Number int + URL string + IsCurrent bool +} + +func New(configs *kon.Kon, total, perPage, currentPage int, url string) *Pagination { + pagination := &Pagination{ + total: total, + perPage: perPage, + currentPage: currentPage, + url: url, + maxPagesToShow: 5, + lastPage: math.Max(float64(int(math.Ceil(float64(total)/float64(perPage)))), float64(1)), + template: defaultTemplate, + configs: configs, + } + + pagination.updateNumPages() + + return pagination +} + +func (p *Pagination) updateNumPages() { + p.numPages = int(math.Ceil(float64(p.total) / float64(p.perPage))) +} + +// SetTemplate - set pagination template. +func (p *Pagination) SetTemplate(name string) { + p.template = name +} + +// Pages - calculate pagination pages. +func (p *Pagination) Pages() []Page { + var pages []Page + var slidingStart int + var slidingEnd int + + if p.numPages <= 1 { + return make([]Page, 0) + } + + if p.numPages <= p.maxPagesToShow { + for i := 1; i <= p.numPages; i++ { + pages = append(pages, p.createPage(i, i == p.currentPage)) + } + } else { + numAdjacents := int(math.Floor(float64((p.maxPagesToShow - 3) / 2))) + if (p.currentPage + numAdjacents) > p.numPages { + // $slidingStart = $this->numPages - $this->maxPagesToShow + 2; + slidingStart = p.numPages - p.maxPagesToShow + 2 + } else { + slidingStart = p.currentPage - numAdjacents + } + if slidingStart < 2 { + slidingStart = 2 + } + + slidingEnd = slidingStart + p.maxPagesToShow - 3 + if slidingEnd >= p.numPages { + slidingEnd = p.numPages - 1 + } + + // Build the list of pages. + pages = append(pages, p.createPage(1, p.currentPage == 1)) + if slidingStart > 2 { + pages = append(pages, p.createPageEllipsis()) + } + + for i := slidingStart; i <= slidingEnd; i++ { + pages = append(pages, p.createPage(i, i == p.currentPage)) + } + + if slidingEnd < p.numPages-1 { + pages = append(pages, p.createPageEllipsis()) + } + + pages = append(pages, p.createPage(p.numPages, p.currentPage == p.numPages)) + } + + return pages +} + +func (p *Pagination) createPage(number int, isCurrent bool) Page { + return Page{ + Number: number, + URL: p.url + helpers.ConvertToString(number), + IsCurrent: isCurrent, + } +} + +func (p *Pagination) createPageEllipsis() Page { + return Page{ + Number: 0, + URL: "", + IsCurrent: false, + } +} + +// OnFirstPage - check if we in first page or not. +func (p *Pagination) OnFirstPage() bool { + return p.currentPage <= 1 +} + +// HasMorePages - check if we have more pages. +func (p *Pagination) HasMorePages() bool { + return p.currentPage < int(p.lastPage) +} + +// PrevPage - return prev page number. +func (p *Pagination) PrevPage() int { + if p.currentPage > 1 { + return p.currentPage - 1 + } + return 0 +} + +// PrevURL - return prev page url. +func (p *Pagination) PrevURL() string { + if p.PrevPage() == 0 { + return "" + } + + return p.url + helpers.ConvertToString(p.PrevPage()) +} + +// NextPage - return nrxt page number. +func (p *Pagination) NextPage() int { + if p.currentPage < p.numPages { + return p.currentPage + 1 + } + return 0 +} + +// NextURL - return next page url. +func (p *Pagination) NextURL() string { + if p.NextPage() == 0 { + return "" + } + + return p.url + helpers.ConvertToString(p.NextPage()) +} + +func getTemplate(template string) string { + if template == "" { + return defaultTemplate + } + + return template +} + +// Render - render the pagination. +func (p *Pagination) Render() template.HTML { + var out bytes.Buffer + tmpl := template.Must(template.ParseFiles("./storage/framework/views/pagination/" + getTemplate(p.configs.GetString("PAGINATION_TEMPLATE")) + ".go.html")) + + err := tmpl.Execute(&out, p) + if err != nil { + log.Fatalf("template execution: %s", err) + } + + return template.HTML(out.String()) + +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..fc85c4f --- /dev/null +++ b/router.go @@ -0,0 +1,198 @@ +package kira + +import ( + "net/http" + "os" + "strconv" + + "github.com/gorilla/mux" +) + +// Route struct +type Route struct { + Name string + Methods []string + Pattern string + HandlerFunc http.HandlerFunc +} + +// SetName set the route name. +func (r *Route) SetName(name string) *Route { + r.Name = name + return r +} + +// Middleware - set a middleware to the route. +func (r *Route) Middleware(middleware Middleware) *Route { + r.HandlerFunc = middleware.Handler(r.HandlerFunc).ServeHTTP + + return r +} + +// NewRouter return all routes. +func (a *App) NewRouter() *mux.Router { + for _, route := range a.Routes { + var handler http.Handler + + handler = route.HandlerFunc + + // append middlewares. + for _, middleware := range a.Middlewares { + handler = middleware.Handler(handler) + } + + a.Router.Methods(route.Methods...).Path(route.Pattern).Name(route.Name).Handler(handler) + } + + // 404 pages. + var notFoundHandler http.Handler + notFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a.Abort(w, r, 404) + return + }) + for _, middleware := range a.Middlewares { + notFoundHandler = middleware.Handler(notFoundHandler) + } + a.Router.NotFoundHandler = notFoundHandler + + // return router + return a.Router +} + +// Static ... +func (a *App) Static(path, url string) { + a.Router.PathPrefix(url).Handler( + http.StripPrefix(url, + http.FileServer(http.Dir(path)), + ), + ) +} + +// UseRoutes - assign the routes +func (a *App) UseRoutes(m []Route) { + for _, route := range m { + a.Routes = append(a.Routes, &route) + // a.Routes[route.Pattern] = &route + } +} + +// UseRoute for append route to the routes +func (a *App) UseRoute(m Route) { + a.Routes = append(a.Routes, &m) + // a.Routes[m.Pattern] = &m +} + +// Methods ... +func (a *App) Methods(methods []string, pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: methods, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// GET request +func (a *App) GET(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"GET"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// POST request +func (a *App) POST(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"POST"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// PUT request +func (a *App) PUT(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"PUT"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// DELETE request +func (a *App) DELETE(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"DELETE"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// HEAD request +func (a *App) HEAD(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"HEAD"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// OPTIONS request +func (a *App) OPTIONS(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"OPTIONS"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// PATCH request +func (a *App) PATCH(pattern string, handler http.HandlerFunc) *Route { + route := &Route{Methods: []string{"PATCH"}, Pattern: pattern, HandlerFunc: handler} + a.Routes = append(a.Routes, route) + + return route +} + +// // PathPrefix +// func (a *App) PathPrefix(tpl string, handler http.HandlerFunc) *Route { +// hand := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// a.Router.PathPrefix(tpl).Handler(http.StripPrefix(tpl, http.HandlerFunc(handler))) +// }) + +// route := &Route{HandlerFunc: hand} +// a.Routes = append(a.Routes, route) + +// return route +// } + +// Redirect to a url +func (a *App) Redirect(w http.ResponseWriter, req *http.Request, url string) { + http.Redirect(w, req, url, 302) +} + +// RedirectWithCode to a url with code +func (a *App) RedirectWithCode(w http.ResponseWriter, req *http.Request, url string, code int) { + http.Redirect(w, req, url, code) +} + +// RedirectWithError to a url +func (a *App) RedirectWithError(w http.ResponseWriter, req *http.Request, url string, err error) { + a.Session.Flash("error", err.Error()) + + http.Redirect(w, req, url, 302) +} + +// Abort ... +func (a *App) Abort(w http.ResponseWriter, req *http.Request, code int) { + errorPage := "errors/" + strconv.Itoa(code) + // check if the error page template exists + if _, err := os.Stat(a.Configs.GetString("VIEWS_PREFIX") + errorPage + a.Configs.GetString("VIEWS_FILE_SUFFIX")); os.IsNotExist(err) { + errorPage = "errors/500" + } + + w.WriteHeader(code) + a.View.Render(w, req, errorPage) +} + +// Query - return a request query value. +func (a *App) Query(request *http.Request, param string) string { + return request.URL.Query().Get(param) +} + +// Var - return a request var value. +func (a *App) Var(request *http.Request, rvar string) string { + return mux.Vars(request)[rvar] +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..72e73c7 --- /dev/null +++ b/server.go @@ -0,0 +1,70 @@ +package kira + +import ( + "context" + "net/http" + "os" + "os/signal" + "strconv" +) + +// StartServer - Start kira server +func (a *App) StartServer() { + // define the server + server := &http.Server{ + Addr: a.Configs.GetString("SERVER_HOST") + ":" + strconv.Itoa(a.Configs.GetInt("SERVER_PORT")), + Handler: a.Router, + } + + // Gracefully shutdown + go a.GracefullyShutdown(server) + + // Start server + a.Log.Infof("Starting HTTP server, Listening at %q \n", "http://"+server.Addr) + if err := server.ListenAndServe(); err != http.ErrServerClosed { + a.Log.Errorf("%v", err) + } else { + a.Log.Infof("Server closed!") + } +} + +// StartTLSServer - start an TLS server provided by: Let's Encrypt. +func (a *App) StartTLSServer() { + // TODO: + server := &http.Server{ + Addr: a.Configs.GetString("SERVER_HOST") + ":" + strconv.Itoa(a.Configs.GetInt("SERVER_PORT")), + Handler: a.Router, + } + + // Gracefully shutdown + go a.GracefullyShutdown(server) + + // Start server + a.Log.Infof("Starting HTTP server, Listening at %q \n", "https://"+server.Addr) + if err := server.ListenAndServeTLS("storage/framework/cert/server.crt", "storage/framework/cert/server.key"); err != http.ErrServerClosed { + a.Log.Errorf("%v", err) + } else { + a.Log.Infof("Server closed!") + } +} + +// GracefullyShutdown the server +func (a *App) GracefullyShutdown(server *http.Server) { + sigquit := make(chan os.Signal, 1) + signal.Notify(sigquit, os.Interrupt, os.Kill) + + sig := <-sigquit + a.Log.Infof("Caught sig: %+v", sig) + a.Log.Infof("Gracefully shutting down server...") + + if err := server.Shutdown(context.Background()); err != nil { + a.Log.Fatalf("Unable to shutdown server: %v", err) + } else { + a.Log.Infof("Server stopped") + } +} + +// IsTLS - set the tls to true. +func (a *App) IsTLS() { + a.isTLS = true +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..141e17e --- /dev/null +++ b/session.go @@ -0,0 +1,26 @@ +package kira + +import ( + "github.com/Lafriakh/kira/session" + "github.com/go-kira/kon" +) + +// setupSession - for setup the framework session. +func setupSession(config *kon.Kon) *session.Session { + name := config.GetString("SESSION_COOKIE") + + switch config.GetString("SESSION_DRIVER") { + case "file": + return session.NewSession(config, sessionFileHandler(config), name) + } + + return session.NewSession(config, sessionFileHandler(config), name) +} + +// sessionFileHandler - return a file handler for the session. +func sessionFileHandler(config *kon.Kon) session.Handler { + path := config.GetString("SESSION_FILES") + lifetime := config.GetInt("SESSION_COOKIE_LIFETIME") + + return session.NewFileHandler(path, lifetime) +} diff --git a/session/file_handler.go b/session/file_handler.go new file mode 100644 index 0000000..7b71d20 --- /dev/null +++ b/session/file_handler.go @@ -0,0 +1,105 @@ +package session + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/Lafriakh/log" +) + +// FileHandler ... +type FileHandler struct { + Path string + lifetime int64 + filesSuffix string + lock sync.RWMutex +} + +// NewFileHandler return FileHandler instance +func NewFileHandler(path string, lifetime int) *FileHandler { + return &FileHandler{ + Path: path, + lifetime: int64(lifetime), + } +} + +// Read ... +func (f *FileHandler) Read(id string) ([]byte, error) { + filename := filepath.Join(f.Path, "session_"+id) + _, err := os.Stat(filename) + if err != nil { + // if there no file create new one, with empty data + f.Write(id, nil) + + return nil, nil + } + + // read the data from the file + f.lock.RLock() + defer f.lock.RUnlock() + fdata, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + // return the raw data + return fdata, nil +} + +// Write ... +func (f *FileHandler) Write(id string, data []byte) error { + // filename + filename := filepath.Join(f.Path, "session_"+id) + // lock + f.lock.Lock() + defer f.lock.Unlock() + + // write the file + ioutil.WriteFile(filename, data, 0600) + + return nil +} + +// Destroy ... +func (f *FileHandler) Destroy(id string) error { + f.lock.RLock() + defer f.lock.RUnlock() + + // file name + filename := filepath.Join(f.Path, "session_"+id) + + // remove the session file + return os.Remove(filename) +} + +// GC to clean expired sessions. +func (f *FileHandler) GC() { + f.lock.RLock() + defer f.lock.RUnlock() + + // fetch all files inside the sessions folder, and check for expired files to delete them. + if err := filepath.Walk(f.Path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // ignore hidding files. + if strings.HasPrefix(info.Name(), ".") { + return nil + } + + if !info.IsDir() && (info.ModTime().Unix()+f.lifetime) < time.Now().Unix() { + return os.Remove(path) + } + + return nil + }); err != nil { + log.WithContext(log.Fields{ + "files": err, + }).Error("session garbage collecting") + } +} diff --git a/session/handler.go b/session/handler.go new file mode 100644 index 0000000..8b2e671 --- /dev/null +++ b/session/handler.go @@ -0,0 +1,10 @@ +package session + +// Handler ... +// the data will be in map[interface{}]interface{} +type Handler interface { + Read(id string) ([]byte, error) + Write(id string, data []byte) error + Destroy(id string) error + GC() +} diff --git a/session/memory_handler.go b/session/memory_handler.go new file mode 100644 index 0000000..6d3c68d --- /dev/null +++ b/session/memory_handler.go @@ -0,0 +1,53 @@ +package session + +import ( + "sync" + "time" +) + +// MemoryHandler ... +type MemoryHandler struct { + data map[string][]byte + Lifetime int + lock sync.RWMutex +} + +// NewMemoryHandler return MemoryHandler instance +func NewMemoryHandler(path string, lifetime int) *MemoryHandler { + return &MemoryHandler{ + data: make(map[string][]byte), + Lifetime: lifetime, + } +} + +// Read ... +func (m *MemoryHandler) Read(id string) ([]byte, error) { + + // read the data from the file + m.lock.RLock() + defer m.lock.RUnlock() + + // return the raw data + return m.data[id], nil +} + +// Write ... +func (m *MemoryHandler) Write(id string, data []byte) error { + // lock + m.lock.Lock() + defer m.lock.Unlock() + + m.data[id] = data + + return nil +} + +// Destroy ... +func (m *MemoryHandler) Destroy(id string) error { + return nil +} + +// GC ... +func (m *MemoryHandler) GC(maxlifetime time.Time) { + // +} diff --git a/session/options.go b/session/options.go new file mode 100644 index 0000000..ee0f7d0 --- /dev/null +++ b/session/options.go @@ -0,0 +1,75 @@ +package session + +import "github.com/go-kira/kon" + +// Options for session +type Options struct { + Name string + Path string + Domain string + Secure bool + HTTPOnly bool + Lifetime int + ExpireOnClose bool + FilesPath string +} + +func prepareOptions(config *kon.Kon) Options { + var options Options + + // name + if len(options.Name) == 0 { + if config.IsNil("SESSION_COOKIE") { + options.Name = "kira_session" + } else { + options.Name = config.GetString("SESSION_COOKIE") + } + } + // path + if len(options.Path) == 0 { + if config.IsNil("SESSION_COOKIE_PATH") { + options.Path = "/" + } else { + options.Path = config.GetString("SESSION_COOKIE_PATH") + } + + } + // domain + if len(options.Domain) == 0 { + if config.IsNil("SESSION_COOKIE_DOMAIN") { + options.Domain = "" + } else { + options.Domain = config.GetString("SESSION_COOKIE_DOMAIN") + } + } + // secure + if !options.Secure { + options.Secure = config.GetBool("SESSION_COOKIE_SECURE") + } + // http only + if !options.HTTPOnly { + options.HTTPOnly = config.GetBool("SESSION_COOKIE_HTTP_ONLY") + } + // lifetime + if options.Lifetime == 0 { + if config.IsNil("SESSION_LIFETIME") { + options.Lifetime = 3600 + } else { + options.Lifetime = config.GetInt("SESSION_LIFETIME") + } + } + // expired on close + if !options.ExpireOnClose { + options.ExpireOnClose = config.GetBool("SESSION_EXPIRE_ON_CLOSE") + } + // files path + if len(options.FilesPath) == 0 { + if config.IsNil("SESSION_FILES") { + options.FilesPath = "storage/framework/sessions" + } else { + options.FilesPath = config.GetString("SESSION_FILES") + } + } + + return options +} diff --git a/session/options_test.go b/session/options_test.go new file mode 100644 index 0000000..d218b5c --- /dev/null +++ b/session/options_test.go @@ -0,0 +1,16 @@ +package session + +import ( + "fmt" + "testing" +) + +func TestOptions(t *testing.T) { + opt := prepareOptions() + + opt.Path = "/session" + + fmt.Println(opt) + + t.Error("Error") +} diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..75c9988 --- /dev/null +++ b/session/session.go @@ -0,0 +1,133 @@ +package session + +import ( + "encoding/gob" + "net/http" + "time" + + "github.com/Lafriakh/kira/helpers" + "github.com/go-kira/kon" +) + +func init() { + gob.Register(SKV{}) + gob.Register([]interface{}{}) + gob.Register(map[string]string{}) + gob.Register(map[string]error{}) +} + +// SKV - key value type to use it in other handlers +type SKV map[interface{}]interface{} + +// Session ... +type Session struct { + Store *Store + Options Options +} + +// NewSession ... +func NewSession(config *kon.Kon, handler Handler, name string) *Session { + opt := prepareOptions(config) + store := NewStore(name, handler) + // resturn the session + return &Session{ + Store: store, + Options: opt, + } +} + +// SetOptions - to set custom options +func (s *Session) SetOptions(options Options) { + s.Options = options +} + +// StartGC starts GC job in a certain period. +func (s *Session) StartGC(config *kon.Kon) { + // log.Debug("Session GC") + s.Store.GC() + time.AfterFunc(time.Duration(config.GetInt("SESSION_COOKIE_LIFETIME"))*time.Second, func() { + s.StartGC(config) + }) +} + +// All return all session values +func (s *Session) All() SKV { + return s.Store.Values +} + +// Get - get a key / value pair from the session. +func (s *Session) Get(key interface{}) interface{} { + return s.Store.Get(key) +} + +// GetWithDefault - get a key / value pair from the session. +func (s *Session) GetWithDefault(key interface{}, def interface{}) interface{} { + return s.Store.GetWithDefault(key, def) +} + +// Put - put a key / value pair in the session. +func (s *Session) Put(key interface{}, value interface{}) { + s.Store.Put(key, value) +} + +// Push - push a value onto a session slice. +func (s *Session) Push(key interface{}, value interface{}) { + s.Store.Push(key, value) +} + +// Remove - remove session key. +func (s *Session) Remove(keys ...interface{}) { + // remove the key from session + s.Store.Remove(keys...) + + // rewrite the session values without this key / value pair. + // s.Save() +} + +// Has - Checks if a key is present and not nil. +func (s *Session) Has(key interface{}) bool { + return s.Store.Has(key) +} + +// Flush - remove all of the items from the session. +func (s *Session) Flush() { + // remove the key from session + s.Store.Flush() + + // rewrite the session values with empty values. + // s.Save() +} + +// Flash - a key / value pair to the session. +func (s *Session) Flash(key interface{}, value interface{}) { + s.Store.Flash(key, value) +} + +// FlashPush - a key / value pair to the session. +func (s *Session) FlashPush(key interface{}, value interface{}) { + s.Store.FlashPush(key, value) +} + +// Regenerate - generate a new session identifier. +func (s *Session) Regenerate(response http.ResponseWriter) { + // update session id + s.Store.Migrate(false) + // update cookies + helpers.SetCookie(http.Cookie{ + Name: s.Options.Name, + Value: helpers.EncodeBase64([]byte(s.Store.GetID())), + MaxAge: s.Options.Lifetime, + }, response) +} + +// RegenerateWithEmpty - generate a new session identifier. +func (s *Session) RegenerateWithEmpty(response http.ResponseWriter) { + // update session id + s.Store.Migrate(true) + // update cookies + helpers.SetCookie(http.Cookie{ + Name: s.Options.Name, + Value: helpers.EncodeBase64([]byte(s.Store.GetID())), + MaxAge: s.Options.Lifetime, + }, response) +} diff --git a/session/store.go b/session/store.go new file mode 100644 index 0000000..4db5b6d --- /dev/null +++ b/session/store.go @@ -0,0 +1,199 @@ +package session + +import ( + "github.com/Lafriakh/log" + + "github.com/google/uuid" +) + +// Store for manage the session +type Store struct { + id string // session id + Handler Handler + Values map[interface{}]interface{} +} + +// NewStore ... +func NewStore(name string, handler Handler) *Store { + store := &Store{} + store.SetID("") // session id + store.Handler = handler + store.Values = make(SKV) + + // return the store instance + return store +} + +// Start the session, reading the data from a handler. +func (s *Store) Start() { + s.loadSession() +} +func (s *Store) loadSession() { + s.Values = s.ReadFromHandler() +} + +// ReadFromHandler read the data from the handler +func (s *Store) ReadFromHandler() SKV { + readedData, err := s.Handler.Read(s.GetID()) + if err != nil { + log.Panic(err) + return nil + } + + // decode the session data + var data SKV + + // if there no session values, return empty data with no errors + if len(readedData) == 0 { + return make(SKV) + } + + // decode the session data + err = DecodeGob(readedData, &data) + if err != nil { + log.Errorf("session decode: %v", err) + return make(SKV) + } + + return data +} + +// SetID to set the id for the session or use the given id +func (s *Store) SetID(id string) { + if id == "" { + // generate the session id + id = s.generateID() + } + // use the given session id + s.id = id +} + +// GetID - return the session id +func (s *Store) GetID() string { + return s.id +} + +// Save - write the data by the handler +func (s *Store) Save() { + s.AgeFlashData() + + // encode the data from the handler + encData, err := EncodeGob(s.Values) + if err != nil { + log.Panic(err) + } + + s.Handler.Write(s.GetID(), encData) +} + +// DestroyFromHandler - destroy the session +func (s *Store) DestroyFromHandler(id string) error { + return s.Handler.Destroy(id) +} + +// Put - put a key / value pair in the session. +func (s *Store) Put(key interface{}, value interface{}) { + s.Values[key] = value +} + +// Push - push a value onto a session slice. +func (s *Store) Push(key interface{}, value interface{}) { + // init an empty slice + var slice []interface{} + // append any data to the slice if exists. + slice = s.GetWithDefault(key, slice).([]interface{}) + // append the flash data. + slice = append(slice, value) + + // puth the flash data to the session. + s.Put(key, slice) +} + +// Get - get a key / value pair from the session. +func (s *Store) Get(key interface{}) interface{} { + // read data from the file + return s.Values[key] +} + +// GetWithDefault - get a key / value pair from the session. +func (s *Store) GetWithDefault(key interface{}, def interface{}) interface{} { + // if there no value. + if s.Values[key] == nil { + return def + } + + // read data from the file + return s.Values[key] +} + +// Remove - remove the key / value pair from the session. +func (s *Store) Remove(keys ...interface{}) { + for _, val := range keys { + // read data from the file + delete(s.Values, val) + } +} + +// Flush - remove all of the items from the session. +func (s *Store) Flush() { + // remove the key from session + s.Values = make(SKV) +} + +// Has - Checks if a key is present and not nil. +func (s *Store) Has(key interface{}) bool { + _, ok := s.Values[key] + if !ok { + return false + } + + return true +} + +// Flash - a key / value pair to the session. +func (s *Store) Flash(key interface{}, value interface{}) { + s.Put(key, value) + + s.Push("_flash.new", key) +} + +// FlashPush - a key / value pair to the session. +func (s *Store) FlashPush(key interface{}, value interface{}) { + s.Push(key, value) + + s.Push("_flash.new", key) +} + +// AgeFlashData - Age the flash data for the session. +func (s *Store) AgeFlashData() { + oldFlashes := s.GetWithDefault("_flash.old", []interface{}{}).([]interface{}) + newFlashes := s.GetWithDefault("_flash.new", []interface{}{}).([]interface{}) + // remove old flashes + s.Remove(oldFlashes...) + // put new flashes into old flashes. + s.Put("_flash.old", newFlashes) + // make new flashes empty + s.Put("_flash.new", []interface{}{}) +} + +// Migrate - generate a new session ID for the session. +func (s *Store) Migrate(empty bool) { + if empty { + s.Values = make(SKV) + } + + // remove old data from handler + s.DestroyFromHandler(s.id) + + // generate new session id + s.SetID(s.generateID()) +} + +func (s *Store) generateID() string { + return uuid.New().String() +} + +// GC - Garbage collector, to remove old session. +func (s *Store) GC() { + s.Handler.GC() +} diff --git a/session/util.go b/session/util.go new file mode 100644 index 0000000..5c801c9 --- /dev/null +++ b/session/util.go @@ -0,0 +1,76 @@ +package session + +import ( + "bytes" + "encoding/gob" + "net/http" + "time" + + "github.com/Lafriakh/kira/helpers" + "github.com/go-kira/kon" +) + +// EncodeGob the session data +func EncodeGob(input interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + encCache := gob.NewEncoder(buf) + err := encCache.Encode(input) + + return buf.Bytes(), err +} + +// DecodeGob the session data +func DecodeGob(input []byte, data interface{}) error { + buf := bytes.NewBuffer(input) + decCache := gob.NewDecoder(buf) + err := decCache.Decode(data) + + return err +} + +// GetMaxLifetime - this return the time type when the session expired. +func GetMaxLifetime(seconds int) time.Time { + lifetime := time.Duration(seconds) * time.Second + + return time.Now().Add(lifetime) +} + +// ParseID - take an id and convert it to base64. +func ParseID(id string) string { + base64, err := helpers.DecodeBase64(id) + if err != nil { + return "" + } + return string(base64) +} + +// NewCookie returns an http.Cookie with the options set. It also sets +// the Expires field calculated based on the MaxAge value, for Internet +// Explorer compatibility. +func NewCookie(config *kon.Kon, name, value string, options Options) *http.Cookie { + // save cookie + cookie := &http.Cookie{ + Name: name, + Value: value, + Path: options.Path, + Domain: options.Domain, + MaxAge: options.Lifetime, + Secure: options.Secure, + HttpOnly: options.HTTPOnly, + } + + if options.Lifetime > 0 { + cookie.Expires = GetMaxLifetime(options.Lifetime) + } else if options.Lifetime < 0 { + // Set it to the past to expire now. + cookie.Expires = time.Unix(1, 0) + } + + // if SESSION_EXPIRE_ON_CLOSE = true + if config.GetBool("SESSION_EXPIRE_ON_CLOSE") { + cookie.Expires = GetMaxLifetime(options.Lifetime).Add(-10 * time.Minute) + cookie.MaxAge = -1 + } + + return cookie +} diff --git a/upload/image.go b/upload/image.go new file mode 100644 index 0000000..2da40ef --- /dev/null +++ b/upload/image.go @@ -0,0 +1,31 @@ +package upload + +import ( + "image" + "image/jpeg" + "image/png" + "io" + + // this for convert any image type to specefic one. + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +// convertToPNG converts from any recognized format to PNG. +func convertToPNG(w io.Writer, r io.Reader) error { + img, _, err := image.Decode(r) + if err != nil { + return err + } + return png.Encode(w, img) +} + +// convertToJPEG converts from any recognized format to JPEG. +func convertToJPEG(w io.Writer, r io.Reader) error { + img, _, err := image.Decode(r) + if err != nil { + return err + } + return jpeg.Encode(w, img, nil) +} diff --git a/upload/mimes.go b/upload/mimes.go new file mode 100644 index 0000000..2445ac5 --- /dev/null +++ b/upload/mimes.go @@ -0,0 +1,770 @@ +package upload + +// Mimes ... +var Mimes = map[string]string{ + "ez": "application/andrew-inset", + "aw": "application/applixware", + "atom": "application/atom+xml", + "atomcat": "application/atomcat+xml", + "atomsvc": "application/atomsvc+xml", + "ccxml": "application/ccxml+xml", + "cdmia": "application/cdmi-capability", + "cdmic": "application/cdmi-container", + "cdmid": "application/cdmi-domain", + "cdmio": "application/cdmi-object", + "cdmiq": "application/cdmi-queue", + "cu": "application/cu-seeme", + "davmount": "application/davmount+xml", + "dbk": "application/docbook+xml", + "dssc": "application/dssc+der", + "xdssc": "application/dssc+xml", + "ecma": "application/ecmascript", + "emma": "application/emma+xml", + "epub": "application/epub+zip", + "exi": "application/exi", + "pfr": "application/font-tdpfr", + "gml": "application/gml+xml", + "gpx": "application/gpx+xml", + "gxf": "application/gxf", + "stk": "application/hyperstudio", + "ink": "application/inkml+xml", + "ipfix": "application/ipfix", + "jar": "application/java-archive", + "ser": "application/java-serialized-object", + "class": "application/java-vm", + "js": "application/javascript", + "json": "application/json", + "jsonml": "application/jsonml+json", + "lostxml": "application/lost+xml", + "hqx": "application/mac-binhex40", + "cpt": "application/mac-compactpro", + "mads": "application/mads+xml", + "mrc": "application/marc", + "mrcx": "application/marcxml+xml", + "ma": "application/mathematica", + "mathml": "application/mathml+xml", + "mbox": "application/mbox", + "mscml": "application/mediaservercontrol+xml", + "metalink": "application/metalink+xml", + "meta4": "application/metalink4+xml", + "mets": "application/mets+xml", + "mods": "application/mods+xml", + "m21": "application/mp21", + "mp4s": "application/mp4", + "doc": "application/msword", + "mxf": "application/mxf", + "bin": "application/octet-stream", + "oda": "application/oda", + "opf": "application/oebps-package+xml", + "ogx": "application/ogg", + "omdoc": "application/omdoc+xml", + "onetoc": "application/onenote", + "oxps": "application/oxps", + "xer": "application/patch-ops-error+xml", + "pdf": "application/pdf", + "pgp": "application/pgp-encrypted", + "asc": "application/pgp-signature", + "prf": "application/pics-rules", + "p10": "application/pkcs10", + "p7m": "application/pkcs7-mime", + "p7s": "application/pkcs7-signature", + "p8": "application/pkcs8", + "ac": "application/pkix-attr-cert", + "cer": "application/pkix-cert", + "crl": "application/pkix-crl", + "pkipath": "application/pkix-pkipath", + "pki": "application/pkixcmp", + "pls": "application/pls+xml", + "ai": "application/postscript", + "cww": "application/prs.cww", + "pskcxml": "application/pskc+xml", + "rdf": "application/rdf+xml", + "rif": "application/reginfo+xml", + "rnc": "application/relax-ng-compact-syntax", + "rl": "application/resource-lists+xml", + "rld": "application/resource-lists-diff+xml", + "rs": "application/rls-services+xml", + "gbr": "application/rpki-ghostbusters", + "mft": "application/rpki-manifest", + "roa": "application/rpki-roa", + "rsd": "application/rsd+xml", + "rss": "application/rss+xml", + "sbml": "application/sbml+xml", + "scq": "application/scvp-cv-request", + "scs": "application/scvp-cv-response", + "spq": "application/scvp-vp-request", + "spp": "application/scvp-vp-response", + "sdp": "application/sdp", + "setpay": "application/set-payment-initiation", + "setreg": "application/set-registration-initiation", + "shf": "application/shf+xml", + "smi": "application/smil+xml", + "rq": "application/sparql-query", + "srx": "application/sparql-results+xml", + "gram": "application/srgs", + "grxml": "application/srgs+xml", + "sru": "application/sru+xml", + "ssdl": "application/ssdl+xml", + "ssml": "application/ssml+xml", + "tei": "application/tei+xml", + "tfi": "application/thraud+xml", + "tsd": "application/timestamped-data", + "plb": "application/vnd.3gpp.pic-bw-large", + "psb": "application/vnd.3gpp.pic-bw-small", + "pvb": "application/vnd.3gpp.pic-bw-var", + "tcap": "application/vnd.3gpp2.tcap", + "pwn": "application/vnd.3m.post-it-notes", + "aso": "application/vnd.accpac.simply.aso", + "imp": "application/vnd.accpac.simply.imp", + "acu": "application/vnd.acucobol", + "atc": "application/vnd.acucorp", + "air": "application/vnd.adobe.air-application-installer-package+zip", + "fcdt": "application/vnd.adobe.formscentral.fcdt", + "fxp": "application/vnd.adobe.fxp", + "xdp": "application/vnd.adobe.xdp+xml", + "xfdf": "application/vnd.adobe.xfdf", + "ahead": "application/vnd.ahead.space", + "azf": "application/vnd.airzip.filesecure.azf", + "azs": "application/vnd.airzip.filesecure.azs", + "azw": "application/vnd.amazon.ebook", + "acc": "application/vnd.americandynamics.acc", + "ami": "application/vnd.amiga.ami", + "apk": "application/vnd.android.package-archive", + "cii": "application/vnd.anser-web-certificate-issue-initiation", + "fti": "application/vnd.anser-web-funds-transfer-initiation", + "atx": "application/vnd.antix.game-component", + "mpkg": "application/vnd.apple.installer+xml", + "m3u8": "application/vnd.apple.mpegurl", + "swi": "application/vnd.aristanetworks.swi", + "iota": "application/vnd.astraea-software.iota", + "aep": "application/vnd.audiograph", + "mpm": "application/vnd.blueice.multipass", + "bmi": "application/vnd.bmi", + "rep": "application/vnd.businessobjects", + "cdxml": "application/vnd.chemdraw+xml", + "mmd": "application/vnd.chipnuts.karaoke-mmd", + "cdy": "application/vnd.cinderella", + "cla": "application/vnd.claymore", + "rp9": "application/vnd.cloanto.rp9", + "c4g": "application/vnd.clonk.c4group", + "c11amc": "application/vnd.cluetrust.cartomobile-config", + "c11amz": "application/vnd.cluetrust.cartomobile-config-pkg", + "csp": "application/vnd.commonspace", + "cdbcmsg": "application/vnd.contact.cmsg", + "cmc": "application/vnd.cosmocaller", + "clkx": "application/vnd.crick.clicker", + "clkk": "application/vnd.crick.clicker.keyboard", + "clkp": "application/vnd.crick.clicker.palette", + "clkt": "application/vnd.crick.clicker.template", + "clkw": "application/vnd.crick.clicker.wordbank", + "wbs": "application/vnd.criticaltools.wbs+xml", + "pml": "application/vnd.ctc-posml", + "ppd": "application/vnd.cups-ppd", + "car": "application/vnd.curl.car", + "pcurl": "application/vnd.curl.pcurl", + "dart": "application/vnd.dart", + "rdz": "application/vnd.data-vision.rdz", + "uvf": "application/vnd.dece.data", + "uvt": "application/vnd.dece.ttml+xml", + "uvx": "application/vnd.dece.unspecified", + "uvz": "application/vnd.dece.zip", + "fe_launch": "application/vnd.denovo.fcselayout-link", + "dna": "application/vnd.dna", + "mlp": "application/vnd.dolby.mlp", + "dpg": "application/vnd.dpgraph", + "dfac": "application/vnd.dreamfactory", + "kpxx": "application/vnd.ds-keypoint", + "ait": "application/vnd.dvb.ait", + "svc": "application/vnd.dvb.service", + "geo": "application/vnd.dynageo", + "mag": "application/vnd.ecowin.chart", + "nml": "application/vnd.enliven", + "esf": "application/vnd.epson.esf", + "msf": "application/vnd.epson.msf", + "qam": "application/vnd.epson.quickanime", + "slt": "application/vnd.epson.salt", + "ssf": "application/vnd.epson.ssf", + "es3": "application/vnd.eszigno3+xml", + "ez2": "application/vnd.ezpix-album", + "ez3": "application/vnd.ezpix-package", + "fdf": "application/vnd.fdf", + "mseed": "application/vnd.fdsn.mseed", + "seed": "application/vnd.fdsn.seed", + "gph": "application/vnd.flographit", + "ftc": "application/vnd.fluxtime.clip", + "fm": "application/vnd.framemaker", + "fnc": "application/vnd.frogans.fnc", + "ltf": "application/vnd.frogans.ltf", + "fsc": "application/vnd.fsc.weblaunch", + "oas": "application/vnd.fujitsu.oasys", + "oa2": "application/vnd.fujitsu.oasys2", + "oa3": "application/vnd.fujitsu.oasys3", + "fg5": "application/vnd.fujitsu.oasysgp", + "bh2": "application/vnd.fujitsu.oasysprs", + "ddd": "application/vnd.fujixerox.ddd", + "xdw": "application/vnd.fujixerox.docuworks", + "xbd": "application/vnd.fujixerox.docuworks.binder", + "fzs": "application/vnd.fuzzysheet", + "txd": "application/vnd.genomatix.tuxedo", + "ggb": "application/vnd.geogebra.file", + "ggt": "application/vnd.geogebra.tool", + "gex": "application/vnd.geometry-explorer", + "gxt": "application/vnd.geonext", + "g2w": "application/vnd.geoplan", + "g3w": "application/vnd.geospace", + "gmx": "application/vnd.gmx", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "gqf": "application/vnd.grafeq", + "gac": "application/vnd.groove-account", + "ghf": "application/vnd.groove-help", + "gim": "application/vnd.groove-identity-message", + "grv": "application/vnd.groove-injector", + "gtm": "application/vnd.groove-tool-message", + "tpl": "application/vnd.groove-tool-template", + "vcg": "application/vnd.groove-vcard", + "hal": "application/vnd.hal+xml", + "zmm": "application/vnd.handheld-entertainment+xml", + "hbci": "application/vnd.hbci", + "les": "application/vnd.hhe.lesson-player", + "hpgl": "application/vnd.hp-hpgl", + "hpid": "application/vnd.hp-hpid", + "hps": "application/vnd.hp-hps", + "jlt": "application/vnd.hp-jlyt", + "pcl": "application/vnd.hp-pcl", + "pclxl": "application/vnd.hp-pclxl", + "mpy": "application/vnd.ibm.minipay", + "afp": "application/vnd.ibm.modcap", + "irm": "application/vnd.ibm.rights-management", + "sc": "application/vnd.ibm.secure-container", + "icc": "application/vnd.iccprofile", + "igl": "application/vnd.igloader", + "ivp": "application/vnd.immervision-ivp", + "ivu": "application/vnd.immervision-ivu", + "igm": "application/vnd.insors.igm", + "xpw": "application/vnd.intercon.formnet", + "i2g": "application/vnd.intergeo", + "qbo": "application/vnd.intu.qbo", + "qfx": "application/vnd.intu.qfx", + "rcprofile": "application/vnd.ipunplugged.rcprofile", + "irp": "application/vnd.irepository.package+xml", + "xpr": "application/vnd.is-xpr", + "fcs": "application/vnd.isac.fcs", + "jam": "application/vnd.jam", + "rms": "application/vnd.jcp.javame.midlet-rms", + "jisp": "application/vnd.jisp", + "joda": "application/vnd.joost.joda-archive", + "ktz": "application/vnd.kahootz", + "karbon": "application/vnd.kde.karbon", + "chrt": "application/vnd.kde.kchart", + "kfo": "application/vnd.kde.kformula", + "flw": "application/vnd.kde.kivio", + "kon": "application/vnd.kde.kontour", + "kpr": "application/vnd.kde.kpresenter", + "ksp": "application/vnd.kde.kspread", + "kwd": "application/vnd.kde.kword", + "htke": "application/vnd.kenameaapp", + "kia": "application/vnd.kidspiration", + "kne": "application/vnd.kinar", + "skp": "application/vnd.koan", + "sse": "application/vnd.kodak-descriptor", + "lasxml": "application/vnd.las.las+xml", + "lbd": "application/vnd.llamagraphics.life-balance.desktop", + "lbe": "application/vnd.llamagraphics.life-balance.exchange+xml", + "123": "application/vnd.lotus-1-2-3", + "apr": "application/vnd.lotus-approach", + "pre": "application/vnd.lotus-freelance", + "nsf": "application/vnd.lotus-notes", + "org": "application/vnd.lotus-organizer", + "scm": "application/vnd.lotus-screencam", + "lwp": "application/vnd.lotus-wordpro", + "portpkg": "application/vnd.macports.portpkg", + "mcd": "application/vnd.mcd", + "mc1": "application/vnd.medcalcdata", + "cdkey": "application/vnd.mediastation.cdkey", + "mwf": "application/vnd.mfer", + "mfm": "application/vnd.mfmp", + "flo": "application/vnd.micrografx.flo", + "igx": "application/vnd.micrografx.igx", + "mif": "application/vnd.mif", + "daf": "application/vnd.mobius.daf", + "dis": "application/vnd.mobius.dis", + "mbk": "application/vnd.mobius.mbk", + "mqy": "application/vnd.mobius.mqy", + "msl": "application/vnd.mobius.msl", + "plc": "application/vnd.mobius.plc", + "txf": "application/vnd.mobius.txf", + "mpn": "application/vnd.mophun.application", + "mpc": "application/vnd.mophun.certificate", + "xul": "application/vnd.mozilla.xul+xml", + "cil": "application/vnd.ms-artgalry", + "cab": "application/vnd.ms-cab-compressed", + "xls": "application/vnd.ms-excel", + "xlam": "application/vnd.ms-excel.addin.macroenabled.12", + "xlsb": "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "xlsm": "application/vnd.ms-excel.sheet.macroenabled.12", + "xltm": "application/vnd.ms-excel.template.macroenabled.12", + "eot": "application/vnd.ms-fontobject", + "chm": "application/vnd.ms-htmlhelp", + "ims": "application/vnd.ms-ims", + "lrm": "application/vnd.ms-lrm", + "thmx": "application/vnd.ms-officetheme", + "cat": "application/vnd.ms-pki.seccat", + "stl": "application/vnd.ms-pki.stl", + "ppt": "application/vnd.ms-powerpoint", + "ppam": "application/vnd.ms-powerpoint.addin.macroenabled.12", + "pptm": "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "sldm": "application/vnd.ms-powerpoint.slide.macroenabled.12", + "ppsm": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "potm": "application/vnd.ms-powerpoint.template.macroenabled.12", + "mpp": "application/vnd.ms-project", + "docm": "application/vnd.ms-word.document.macroenabled.12", + "dotm": "application/vnd.ms-word.template.macroenabled.12", + "wps": "application/vnd.ms-works", + "wpl": "application/vnd.ms-wpl", + "xps": "application/vnd.ms-xpsdocument", + "mseq": "application/vnd.mseq", + "mus": "application/vnd.musician", + "msty": "application/vnd.muvee.style", + "taglet": "application/vnd.mynfc", + "nlu": "application/vnd.neurolanguage.nlu", + "ntf": "application/vnd.nitf", + "nnd": "application/vnd.noblenet-directory", + "nns": "application/vnd.noblenet-sealer", + "nnw": "application/vnd.noblenet-web", + "ngdat": "application/vnd.nokia.n-gage.data", + "rpst": "application/vnd.nokia.radio-preset", + "rpss": "application/vnd.nokia.radio-presets", + "edm": "application/vnd.novadigm.edm", + "edx": "application/vnd.novadigm.edx", + "ext": "application/vnd.novadigm.ext", + "odc": "application/vnd.oasis.opendocument.chart", + "otc": "application/vnd.oasis.opendocument.chart-template", + "odb": "application/vnd.oasis.opendocument.database", + "odf": "application/vnd.oasis.opendocument.formula", + "odft": "application/vnd.oasis.opendocument.formula-template", + "odg": "application/vnd.oasis.opendocument.graphics", + "otg": "application/vnd.oasis.opendocument.graphics-template", + "odi": "application/vnd.oasis.opendocument.image", + "oti": "application/vnd.oasis.opendocument.image-template", + "odp": "application/vnd.oasis.opendocument.presentation", + "otp": "application/vnd.oasis.opendocument.presentation-template", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "ots": "application/vnd.oasis.opendocument.spreadsheet-template", + "odt": "application/vnd.oasis.opendocument.text", + "odm": "application/vnd.oasis.opendocument.text-master", + "ott": "application/vnd.oasis.opendocument.text-template", + "oth": "application/vnd.oasis.opendocument.text-web", + "xo": "application/vnd.olpc-sugar", + "dd2": "application/vnd.oma.dd2+xml", + "oxt": "application/vnd.openofficeorg.extension", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", + "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "potx": "application/vnd.openxmlformats-officedocument.presentationml.template", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "mgp": "application/vnd.osgeo.mapguide.package", + "dp": "application/vnd.osgi.dp", + "esa": "application/vnd.osgi.subsystem", + "pdb": "application/vnd.palm", + "paw": "application/vnd.pawaafile", + "str": "application/vnd.pg.format", + "ei6": "application/vnd.pg.osasli", + "efif": "application/vnd.picsel", + "wg": "application/vnd.pmi.widget", + "plf": "application/vnd.pocketlearn", + "pbd": "application/vnd.powerbuilder6", + "box": "application/vnd.previewsystems.box", + "mgz": "application/vnd.proteus.magazine", + "qps": "application/vnd.publishare-delta-tree", + "ptid": "application/vnd.pvi.ptid1", + "qxd": "application/vnd.quark.quarkxpress", + "bed": "application/vnd.realvnc.bed", + "mxl": "application/vnd.recordare.musicxml", + "musicxml": "application/vnd.recordare.musicxml+xml", + "cryptonote": "application/vnd.rig.cryptonote", + "cod": "application/vnd.rim.cod", + "rm": "application/vnd.rn-realmedia", + "rmvb": "application/vnd.rn-realmedia-vbr", + "link66": "application/vnd.route66.link66+xml", + "st": "application/vnd.sailingtracker.track", + "see": "application/vnd.seemail", + "sema": "application/vnd.sema", + "semd": "application/vnd.semd", + "semf": "application/vnd.semf", + "ifm": "application/vnd.shana.informed.formdata", + "itp": "application/vnd.shana.informed.formtemplate", + "iif": "application/vnd.shana.informed.interchange", + "ipk": "application/vnd.shana.informed.package", + "twd": "application/vnd.simtech-mindmapper", + "mmf": "application/vnd.smaf", + "teacher": "application/vnd.smart.teacher", + "sdkm": "application/vnd.solent.sdkm+xml", + "dxp": "application/vnd.spotfire.dxp", + "sfs": "application/vnd.spotfire.sfs", + "sdc": "application/vnd.stardivision.calc", + "sda": "application/vnd.stardivision.draw", + "sdd": "application/vnd.stardivision.impress", + "smf": "application/vnd.stardivision.math", + "sdw": "application/vnd.stardivision.writer", + "sgl": "application/vnd.stardivision.writer-global", + "smzip": "application/vnd.stepmania.package", + "sm": "application/vnd.stepmania.stepchart", + "sxc": "application/vnd.sun.xml.calc", + "stc": "application/vnd.sun.xml.calc.template", + "sxd": "application/vnd.sun.xml.draw", + "std": "application/vnd.sun.xml.draw.template", + "sxi": "application/vnd.sun.xml.impress", + "sti": "application/vnd.sun.xml.impress.template", + "sxm": "application/vnd.sun.xml.math", + "sxw": "application/vnd.sun.xml.writer", + "sxg": "application/vnd.sun.xml.writer.global", + "stw": "application/vnd.sun.xml.writer.template", + "sus": "application/vnd.sus-calendar", + "svd": "application/vnd.svd", + "sis": "application/vnd.symbian.install", + "xsm": "application/vnd.syncml+xml", + "bdm": "application/vnd.syncml.dm+wbxml", + "xdm": "application/vnd.syncml.dm+xml", + "tao": "application/vnd.tao.intent-module-archive", + "pcap": "application/vnd.tcpdump.pcap", + "tmo": "application/vnd.tmobile-livetv", + "tpt": "application/vnd.trid.tpt", + "mxs": "application/vnd.triscape.mxs", + "tra": "application/vnd.trueapp", + "ufd": "application/vnd.ufdl", + "utz": "application/vnd.uiq.theme", + "umj": "application/vnd.umajin", + "unityweb": "application/vnd.unity", + "uoml": "application/vnd.uoml+xml", + "vcx": "application/vnd.vcx", + "vsd": "application/vnd.visio", + "vis": "application/vnd.visionary", + "vsf": "application/vnd.vsf", + "wbxml": "application/vnd.wap.wbxml", + "wmlc": "application/vnd.wap.wmlc", + "wmlsc": "application/vnd.wap.wmlscriptc", + "wtb": "application/vnd.webturbo", + "nbp": "application/vnd.wolfram.player", + "wpd": "application/vnd.wordperfect", + "wqd": "application/vnd.wqd", + "stf": "application/vnd.wt.stf", + "xar": "application/vnd.xara", + "xfdl": "application/vnd.xfdl", + "hvd": "application/vnd.yamaha.hv-dic", + "hvs": "application/vnd.yamaha.hv-script", + "hvp": "application/vnd.yamaha.hv-voice", + "osf": "application/vnd.yamaha.openscoreformat", + "osfpvg": "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "saf": "application/vnd.yamaha.smaf-audio", + "spf": "application/vnd.yamaha.smaf-phrase", + "cmp": "application/vnd.yellowriver-custom-menu", + "zir": "application/vnd.zul", + "zaz": "application/vnd.zzazz.deck+xml", + "vxml": "application/voicexml+xml", + "wgt": "application/widget", + "hlp": "application/winhlp", + "wsdl": "application/wsdl+xml", + "wspolicy": "application/wspolicy+xml", + "7z": "application/x-7z-compressed", + "abw": "application/x-abiword", + "ace": "application/x-ace-compressed", + "dmg": "application/x-apple-diskimage", + "aab": "application/x-authorware-bin", + "aam": "application/x-authorware-map", + "aas": "application/x-authorware-seg", + "bcpio": "application/x-bcpio", + "torrent": "application/x-bittorrent", + "blb": "application/x-blorb", + "bz": "application/x-bzip", + "bz2": "application/x-bzip2", + "cbr": "application/x-cbr", + "vcd": "application/x-cdlink", + "cfs": "application/x-cfs-compressed", + "chat": "application/x-chat", + "pgn": "application/x-chess-pgn", + "nsc": "application/x-conference", + "cpio": "application/x-cpio", + "csh": "application/x-csh", + "deb": "application/x-debian-package", + "dgc": "application/x-dgc-compressed", + "dir": "application/x-director", + "wad": "application/x-doom", + "ncx": "application/x-dtbncx+xml", + "dtb": "application/x-dtbook+xml", + "res": "application/x-dtbresource+xml", + "dvi": "application/x-dvi", + "evy": "application/x-envoy", + "eva": "application/x-eva", + "bdf": "application/x-font-bdf", + "gsf": "application/x-font-ghostscript", + "psf": "application/x-font-linux-psf", + "otf": "application/x-font-otf", + "pcf": "application/x-font-pcf", + "snf": "application/x-font-snf", + "ttf": "application/x-font-ttf", + "pfa": "application/x-font-type1", + "woff": "application/x-font-woff", + "arc": "application/x-freearc", + "spl": "application/x-futuresplash", + "gca": "application/x-gca-compressed", + "ulx": "application/x-glulx", + "gnumeric": "application/x-gnumeric", + "gramps": "application/x-gramps-xml", + "gtar": "application/x-gtar", + "hdf": "application/x-hdf", + "install": "application/x-install-instructions", + "iso": "application/x-iso9660-image", + "jnlp": "application/x-java-jnlp-file", + "latex": "application/x-latex", + "lzh": "application/x-lzh-compressed", + "mie": "application/x-mie", + "prc": "application/x-mobipocket-ebook", + "application": "application/x-ms-application", + "lnk": "application/x-ms-shortcut", + "wmd": "application/x-ms-wmd", + "wmz": "application/x-ms-wmz", + "xbap": "application/x-ms-xbap", + "mdb": "application/x-msaccess", + "obd": "application/x-msbinder", + "crd": "application/x-mscardfile", + "clp": "application/x-msclip", + "exe": "application/x-msdownload", + "mvb": "application/x-msmediaview", + "wmf": "application/x-msmetafile", + "mny": "application/x-msmoney", + "pub": "application/x-mspublisher", + "scd": "application/x-msschedule", + "trm": "application/x-msterminal", + "wri": "application/x-mswrite", + "nc": "application/x-netcdf", + "nzb": "application/x-nzb", + "p12": "application/x-pkcs12", + "p7b": "application/x-pkcs7-certificates", + "p7r": "application/x-pkcs7-certreqresp", + "rar": "application/x-rar", + "ris": "application/x-research-info-systems", + "sh": "application/x-sh", + "shar": "application/x-shar", + "swf": "application/x-shockwave-flash", + "xap": "application/x-silverlight-app", + "sql": "application/x-sql", + "sit": "application/x-stuffit", + "sitx": "application/x-stuffitx", + "srt": "application/x-subrip", + "sv4cpio": "application/x-sv4cpio", + "sv4crc": "application/x-sv4crc", + "t3": "application/x-t3vm-image", + "gam": "application/x-tads", + "tar": "application/x-tar", + "tcl": "application/x-tcl", + "tex": "application/x-tex", + "tfm": "application/x-tex-tfm", + "texinfo": "application/x-texinfo", + "obj": "application/x-tgif", + "ustar": "application/x-ustar", + "src": "application/x-wais-source", + "der": "application/x-x509-ca-cert", + "fig": "application/x-xfig", + "xlf": "application/x-xliff+xml", + "xpi": "application/x-xpinstall", + "xz": "application/x-xz", + "z1": "application/x-zmachine", + "xaml": "application/xaml+xml", + "xdf": "application/xcap-diff+xml", + "xenc": "application/xenc+xml", + "xhtml": "application/xhtml+xml", + "xml": "application/xml", + "dtd": "application/xml-dtd", + "xop": "application/xop+xml", + "xpl": "application/xproc+xml", + "xslt": "application/xslt+xml", + "xspf": "application/xspf+xml", + "mxml": "application/xv+xml", + "yang": "application/yang", + "yin": "application/yin+xml", + "zip": "application/zip", + "adp": "audio/adpcm", + "au": "audio/basic", + "mid": "audio/midi", + "mp3": "audio/mpeg", + "mp4a": "audio/mp4", + "mpga": "audio/mpeg", + "oga": "audio/ogg", + "s3m": "audio/s3m", + "sil": "audio/silk", + "uva": "audio/vnd.dece.audio", + "eol": "audio/vnd.digital-winds", + "dra": "audio/vnd.dra", + "dts": "audio/vnd.dts", + "dtshd": "audio/vnd.dts.hd", + "lvp": "audio/vnd.lucent.voice", + "pya": "audio/vnd.ms-playready.media.pya", + "ecelp4800": "audio/vnd.nuera.ecelp4800", + "ecelp7470": "audio/vnd.nuera.ecelp7470", + "ecelp9600": "audio/vnd.nuera.ecelp9600", + "rip": "audio/vnd.rip", + "weba": "audio/webm", + "aac": "audio/x-aac", + "aif": "audio/x-aiff", + "caf": "audio/x-caf", + "flac": "audio/x-flac", + "mka": "audio/x-matroska", + "m3u": "audio/x-mpegurl", + "wax": "audio/x-ms-wax", + "wma": "audio/x-ms-wma", + "ram": "audio/x-pn-realaudio", + "rmp": "audio/x-pn-realaudio-plugin", + "wav": "audio/x-wav", + "xm": "audio/xm", + "cdx": "chemical/x-cdx", + "cif": "chemical/x-cif", + "cmdf": "chemical/x-cmdf", + "cml": "chemical/x-cml", + "csml": "chemical/x-csml", + "xyz": "chemical/x-xyz", + "bmp": "image/bmp", + "cgm": "image/cgm", + "g3": "image/g3fax", + "gif": "image/gif", + "ief": "image/ief", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "ktx": "image/ktx", + "png": "image/png", + "btif": "image/prs.btif", + "sgi": "image/sgi", + "svg": "image/svg+xml", + "tiff": "image/tiff", + "psd": "image/vnd.adobe.photoshop", + "uvi": "image/vnd.dece.graphic", + "djvu": "image/vnd.djvu", + "dwg": "image/vnd.dwg", + "dxf": "image/vnd.dxf", + "fbs": "image/vnd.fastbidsheet", + "fpx": "image/vnd.fpx", + "fst": "image/vnd.fst", + "mmr": "image/vnd.fujixerox.edmics-mmr", + "rlc": "image/vnd.fujixerox.edmics-rlc", + "mdi": "image/vnd.ms-modi", + "wdp": "image/vnd.ms-photo", + "npx": "image/vnd.net-fpx", + "wbmp": "image/vnd.wap.wbmp", + "xif": "image/vnd.xiff", + "webp": "image/webp", + "3ds": "image/x-3ds", + "ras": "image/x-cmu-raster", + "cmx": "image/x-cmx", + "fh": "image/x-freehand", + "ico": "image/x-icon", + "sid": "image/x-mrsid-image", + "pcx": "image/x-pcx", + "pic": "image/x-pict", + "pnm": "image/x-portable-anymap", + "pbm": "image/x-portable-bitmap", + "pgm": "image/x-portable-graymap", + "ppm": "image/x-portable-pixmap", + "rgb": "image/x-rgb", + "tga": "image/x-tga", + "xbm": "image/x-xbitmap", + "xpm": "image/x-xpixmap", + "xwd": "image/x-xwindowdump", + "eml": "message/rfc822", + "igs": "model/iges", + "msh": "model/mesh", + "dae": "model/vnd.collada+xml", + "dwf": "model/vnd.dwf", + "gdl": "model/vnd.gdl", + "gtw": "model/vnd.gtw", + "mts": "model/vnd.mts", + "vtu": "model/vnd.vtu", + "wrl": "model/vrml", + "x3db": "model/x3d+binary", + "x3dv": "model/x3d+vrml", + "x3d": "model/x3d+xml", + "appcache": "text/cache-manifest", + "ics": "text/calendar", + "css": "text/css", + "csv": "text/csv", + "html": "text/html", + "n3": "text/n3", + "txt": "text/plain", + "dsc": "text/prs.lines.tag", + "rtx": "text/richtext", + "rtf": "text/rtf", + "sgml": "text/sgml", + "tsv": "text/tab-separated-values", + "t": "text/troff", + "ttl": "text/turtle", + "uri": "text/uri-list", + "vcard": "text/vcard", + "curl": "text/vnd.curl", + "dcurl": "text/vnd.curl.dcurl", + "scurl": "text/vnd.curl.scurl", + "mcurl": "text/vnd.curl.mcurl", + "sub": "text/vnd.dvb.subtitle", + "fly": "text/vnd.fly", + "flx": "text/vnd.fmi.flexstor", + "gv": "text/vnd.graphviz", + "3dml": "text/vnd.in3d.3dml", + "spot": "text/vnd.in3d.spot", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "wmls": "text/vnd.wap.wmlscript", + "s": "text/x-asm", + "c": "text/x-c", + "f": "text/x-fortran", + "p": "text/x-pascal", + "java": "text/x-java-source", + "opml": "text/x-opml", + "nfo": "text/x-nfo", + "etx": "text/x-setext", + "sfv": "text/x-sfv", + "uu": "text/x-uuencode", + "vcs": "text/x-vcalendar", + "vcf": "text/x-vcard", + "3gp": "video/3gpp", + "3g2": "video/3gpp2", + "h261": "video/h261", + "h263": "video/h263", + "h264": "video/h264", + "jpgv": "video/jpeg", + "jpm": "video/jpm", + "mj2": "video/mj2", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "ogv": "video/ogg", + "mov": "video/quicktime", + "qt": "video/quicktime", + "uvh": "video/vnd.dece.hd", + "uvm": "video/vnd.dece.mobile", + "uvp": "video/vnd.dece.pd", + "uvs": "video/vnd.dece.sd", + "uvv": "video/vnd.dece.video", + "dvb": "video/vnd.dvb.file", + "fvt": "video/vnd.fvt", + "mxu": "video/vnd.mpegurl", + "pyv": "video/vnd.ms-playready.media.pyv", + "uvu": "video/vnd.uvvu.mp4", + "viv": "video/vnd.vivo", + "webm": "video/webm", + "f4v": "video/x-f4v", + "fli": "video/x-fli", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "mkv": "video/x-matroska", + "mng": "video/x-mng", + "asf": "video/x-ms-asf", + "vob": "video/x-ms-vob", + "wm": "video/x-ms-wm", + "wmv": "video/x-ms-wmv", + "wmx": "video/x-ms-wmx", + "wvx": "video/x-ms-wvx", + "avi": "video/x-msvideo", + "movie": "video/x-sgi-movie", + "smv": "video/x-smv", + "ice": "x-conference/x-cooltalk", +} diff --git a/upload/upload.go b/upload/upload.go new file mode 100644 index 0000000..e357a42 --- /dev/null +++ b/upload/upload.go @@ -0,0 +1,306 @@ +package upload + +import ( + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + + "github.com/Lafriakh/kira" + "github.com/Lafriakh/kira/helpers" + "github.com/Lafriakh/log" + "github.com/go-kira/kon" +) + +var ( + errNotImage = errors.New("the file not an image") + errWhileWriteFile = errors.New("error while write a file") + errImageToNotFound = errors.New("set an image format to convert to it") + errFileTypeNotAllowed = "the file type: %s not allowed" + errFileSize = "the file %s size: %s too big" + errFileRequired = "the field %s is required" +) + +// MB - one MB. +const ( + KB = 1 << 10 + MB = 1 << 20 + GB = 1 << 30 +) + +var isImage = []string{ + Mimes["jpeg"], + Mimes["png"], + Mimes["gif"], + Mimes["bmp"], + Mimes["svg"], +} + +// Upload - upload struct. +type Upload struct { + Response http.ResponseWriter + Request *http.Request + name, path, form string + isImage bool + imageTo string + ext []string + size int64 + required bool +} + +// Uploaded - is the finale file with returned to the user. +type Uploaded struct { + Name string + Mime string + Size int64 + Path string +} + +// New - return upload instance. +func New(config *kon.Kon, w http.ResponseWriter, r *http.Request) *Upload { + + // return upload instance. + return &Upload{ + Response: w, + Request: r, + size: config.GetInt64("UPLOAD_MAX_SIZE") * MB, // this the default max upload size from config. + required: true, + } +} + +// Form - set the form. +func (u *Upload) Form(form string) *Upload { + u.form = form + + return u +} + +// Name - set the name. +func (u *Upload) Name(name string) *Upload { + u.name = name + + return u +} + +// Path - set the path. +func (u *Upload) Path(path string) *Upload { + u.path = path + + return u +} + +// IsImage - set the upload file as an image. +func (u *Upload) IsImage() *Upload { + u.isImage = true + + return u +} + +// Ext - check if the file that we upload is one of this types. +func (u *Upload) Ext(types []string) *Upload { + u.ext = types + + return u +} + +// Size - to validate the file size. +func (u *Upload) Size(size int64) *Upload { + u.size = size + + return u +} + +// ImageTo - when this set convert the image to this type. +func (u *Upload) ImageTo(itype string) *Upload { + u.imageTo = itype + + return u +} + +// NotRequired - make the form not required. +func (u *Upload) NotRequired() *Upload { + u.required = false + + return u +} + +// Upload - to upload one file. +// dst: ./storage/{dst} +func (u *Upload) Upload() (Uploaded, error) { + var uploaded os.FileInfo + var path string + + // file + file, header, err := u.Request.FormFile(u.form) + if err != nil { + // if the form not required, return nil error. + if !u.required && err == http.ErrMissingFile { + return Uploaded{}, nil + } + return Uploaded{}, err + } + defer file.Close() + + // if the file not required return nil error if file nil and size is 0. + if !u.required && file == nil || !u.required && header.Size == 0 { + return Uploaded{}, nil + } + + // if the file required and the size 0. + if u.required && header.Size == 0 { + return Uploaded{}, fmt.Errorf(errFileRequired, u.form) + } + + // file header + fileHeaderMime, err := u.getFileType(file) + if err != nil { + return Uploaded{}, err + } + + // check if the file type equal to one of the types the user sets. + if len(u.ext) > 0 { + var isOK bool + for _, ext := range u.ext { + if fileHeaderMime == Mimes[ext] { + isOK = true + break + } + } + if !isOK { + // file mime. + typeWithoutDot := filepath.Ext(header.Filename)[1:len(filepath.Ext(header.Filename))] + // return error if the type of the file not allowed. + return Uploaded{}, fmt.Errorf(errFileTypeNotAllowed, typeWithoutDot) + } + } + + // check the file size + if header.Size > u.size { + return Uploaded{}, fmt.Errorf(errFileSize, header.Filename, helpers.BytesFormat(float64(header.Size), 1)) + } + + // upload file + if !u.isImage { + // do upload + path = u.getFilePath(header.Filename) + uploaded, err = writeFile(file, path) + if err != nil { + return Uploaded{}, err + } + } else { + // upload the image + // check if the uploaded file is an image. + if !helpers.Contains(isImage, fileHeaderMime) { + return Uploaded{}, errNotImage + } + if u.imageTo != "" { + // upload image and convert. + path = filepath.Join(u.path, u.name+"."+u.imageTo) + uploaded, err = writeImage(file, path, u.imageTo) + if err != nil { + return Uploaded{}, err + } + } else { + // do upload + path = u.getFilePath(header.Filename) + uploaded, err = writeFile(file, path) + if err != nil { + return Uploaded{}, err + } + } + } + + return Uploaded{ + Name: uploaded.Name(), + Mime: fileHeaderMime, + Size: uploaded.Size(), + Path: path, + }, nil +} + +func (u *Upload) getFileType(file multipart.File) (string, error) { + // Create a buffer to store the header of the file in + fileHeaderBuffer := make([]byte, 512) + // Copy the headers into the FileHeader buffer + if _, err := file.Read(fileHeaderBuffer); err != nil { + return "", err + } + // set position back to start. + if _, err := file.Seek(0, 0); err != nil { + return "", err + } + + return http.DetectContentType(fileHeaderBuffer), nil +} + +func (u *Upload) getFilePath(fname string) string { + return filepath.Join(u.path, u.name+filepath.Ext(fname)) +} + +func writeFile(file multipart.File, dst string) (os.FileInfo, error) { + location := kira.PathApp + dst + + f, err := os.OpenFile(location, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + defer f.Close() + + _, err = io.Copy(f, file) + if err != nil { + return nil, errWhileWriteFile + } + + // return file location. + fileInfo, err := f.Stat() + if err != nil { + log.Panic(err) + } + + return fileInfo, nil +} + +func writeImage(file multipart.File, dst string, utype string) (os.FileInfo, error) { + location := kira.PathApp + dst + + f, err := os.OpenFile(location, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + defer f.Close() + + if utype == "png" { + err = convertToPNG(f, file) + if err != nil { + return nil, err + } + } else if utype == "jpeg" { + err = convertToJPEG(f, file) + if err != nil { + return nil, err + } + } else { + return nil, errImageToNotFound + } + + // return file location. + fileInfo, err := f.Stat() + if err != nil { + log.Panic(err) + } + + return fileInfo, nil +} + +// ParseForm - parse the request form. +func ParseForm(r *http.Request) { + // parse the request before upload. + err := r.ParseMultipartForm(32 << 20) + if err != nil { + log.Panic(err) + } + +} diff --git a/validation.go b/validation.go new file mode 100644 index 0000000..95dc11f --- /dev/null +++ b/validation.go @@ -0,0 +1,30 @@ +package kira + +import ( + "net/http" +) + +// Validate - for request validation. +func (app *App) Validate(request *http.Request, fields map[string]string) map[string]string { + var errs []error + errorMap := make(map[string]string) + for key, rules := range fields { + value := request.FormValue(key) + + if err := app.Validation.Validate(key, value, rules); err != nil { + // append the error to the slice errs + errs = append(errs, err) + errorMap[key] = err.Error() + } + } + + if len(errs) > 0 { + for key, err := range errorMap { + app.Session.FlashPush("errors."+key, err) + } + app.Session.Flash("errors", errorMap) + + return errorMap + } + return nil +} diff --git a/validation/patterns.go b/validation/patterns.go new file mode 100644 index 0000000..241f7d9 --- /dev/null +++ b/validation/patterns.go @@ -0,0 +1,37 @@ +package validation + +import "regexp" + +// Basic regular expressions for validating strings +const ( + Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$" + Alpha string = "^[a-zA-Z]+$" + Alphanumeric string = "^[a-zA-Z0-9]+$" + Numeric string = "^[0-9]+$" + Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$" + Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$" + Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" + DataURI string = "^data:.+\\/(.+);base64$" + IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` + URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)` + URLUsername string = `(\S+(:\S*)?@)` + URLPath string = `((\/|\?|#)[^\s]*)` + URLPort string = `(:(\d{1,5}))` + URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))` + URLSubdomain string = `((www\.)|([a-zA-Z0-9]([-\.][-\._a-zA-Z0-9]+)*))` + URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$` +) + +var ( + rxEmail = regexp.MustCompile(Email) + rxCreditCard = regexp.MustCompile(CreditCard) + rxAlpha = regexp.MustCompile(Alpha) + rxAlphanumeric = regexp.MustCompile(Alphanumeric) + rxNumeric = regexp.MustCompile(Numeric) + rxInt = regexp.MustCompile(Int) + rxFloat = regexp.MustCompile(Float) + rxBase64 = regexp.MustCompile(Base64) + rxDataURI = regexp.MustCompile(DataURI) + rxURL = regexp.MustCompile(URL) +) diff --git a/validation/rules.go b/validation/rules.go new file mode 100644 index 0000000..7e71de9 --- /dev/null +++ b/validation/rules.go @@ -0,0 +1,109 @@ +package validation + +import ( + "fmt" + neturl "net/url" + "strconv" + "strings" + + "github.com/Lafriakh/kira/helpers" +) + +// FuncString this for all rules that accept string as value +type FuncString func(field string, value interface{}, parameters ...string) error + +// RulesNames ... +var RulesNames = map[string]FuncString{ + "required": required, + "integer": integer, + "numeric": numeric, + "max": max, + "min": min, + "between": between, + "email": email, + "url": url, +} + +// Rules ... +func required(field string, value interface{}, parameters ...string) error { + if len(strings.TrimSpace(helpers.ConvertToString(value))) == 0 { + return fmt.Errorf(errRequired, field) + } + // not empty + return nil +} +func integer(field string, value interface{}, parameters ...string) error { + _, err := strconv.Atoi(helpers.ConvertToString(value)) + if err != nil { + return fmt.Errorf(errInteger, field) + } + + // not empty + return nil +} +func numeric(field string, value interface{}, parameters ...string) error { + _, err := strconv.ParseFloat(helpers.ConvertToString(value), 64) + if err != nil { + return fmt.Errorf(errNumeric, field) + } + + // not empty + return nil +} + +func max(field string, value interface{}, parameters ...string) error { + param := helpers.ConvertToFloat64(parameters[0]) // converted to float64 + + if size := helpers.Size(value); size > param { + return fmt.Errorf(errMax, field, param) + } + + // not empty + return nil +} + +func min(field string, value interface{}, parameters ...string) error { + param := helpers.ConvertToFloat64(parameters[0]) // converted to float64 + + if size := helpers.Size(value); size < param { + return fmt.Errorf(errMin, field, param) + } + + // not empty + return nil +} + +func between(field string, value interface{}, parameters ...string) error { + param1 := helpers.ConvertToFloat64(parameters[0]) // converted to float64 + param2 := helpers.ConvertToFloat64(parameters[1]) // converted to float64 + + size := helpers.Size(value) + if size < param1 || size > param2 { + return fmt.Errorf(errBetween, size, param1, param2) + } + + // not empty + return nil +} +func email(field string, value interface{}, parameters ...string) error { + + if isEmail := rxEmail.MatchString(helpers.ConvertToString(value)); isEmail == false { + return fmt.Errorf(errEmail, field) + } + + // not empty + return nil +} + +func url(field string, value interface{}, parameters ...string) error { + if helpers.ConvertToString(value) == "" { + return nil + } + + if _, err := neturl.ParseRequestURI(helpers.ConvertToString(value)); err != nil { + return fmt.Errorf(errURL, field) + } + + // not empty + return nil +} diff --git a/validation/rules_errors.go b/validation/rules_errors.go new file mode 100644 index 0000000..e1d9b74 --- /dev/null +++ b/validation/rules_errors.go @@ -0,0 +1,12 @@ +package validation + +const ( + errRequired = "The %s field is required." + errInteger = "The %s field must contain an integer." + errNumeric = "The %s field must contain only numbers." + errMax = "The %s field cannot exceed %v characters in length." + errMin = "The %s field must be at least %v characters in length." + errBetween = "the given value is %v, is not between %v, %v." + errEmail = "The %s field must contain a valid email address." + errURL = "The %s field must contain a valid url." +) diff --git a/validation/validation.go b/validation/validation.go new file mode 100644 index 0000000..3f54a40 --- /dev/null +++ b/validation/validation.go @@ -0,0 +1,63 @@ +package validation + +import ( + "errors" + "fmt" + "strings" + + "github.com/Lafriakh/kira/helpers" +) + +// Errors +var ( + errorNotString = errors.New("") +) + +// Validation struct +type Validation struct { + Separator string + ParamsPrefix string + ParamsSeparator string +} + +// New return Validation instance +func New() *Validation { + return &Validation{ + Separator: "|", + ParamsPrefix: ":", + ParamsSeparator: ",", + } +} + +// Validate for check the validation +func (v *Validation) Validate(field string, value interface{}, rules string) error { + split := strings.Split(rules, v.Separator) + + for _, rule := range split { + // get before : in the rule + prefix := helpers.Before(rule, ":") + if prefix == "" { + prefix = rule + } + // rule params if exists + suffix := helpers.After(rule, ":") + params := strings.Split(suffix, v.ParamsSeparator) + // nil the params if there no suffix + if suffix == "" { + params = nil + } + + if ruleValue, ruleInMap := RulesNames[prefix]; ruleInMap { + // do the validation + if checkErr := ruleValue(field, value, params...); checkErr != nil { + return checkErr + } + } else { + return fmt.Errorf("the `%s` rule not supported", rule) + } + + } + + // validation pass + return nil +} diff --git a/validation/validation_test.go b/validation/validation_test.go new file mode 100644 index 0000000..f381bf5 --- /dev/null +++ b/validation/validation_test.go @@ -0,0 +1,68 @@ +package validation + +import ( + "testing" +) + +func TestRequired(t *testing.T) { + validation := New() + required := validation.Validate("Fooo", "required") + + if required != nil { + t.Fatal(required) + } +} + +func TestInteger(t *testing.T) { + validation := New() + integer := validation.Validate("50", "integer") + + if integer != nil { + t.Fatal(integer) + } +} + +func TestNumeric(t *testing.T) { + validation := New() + numeric := validation.Validate(50.5, "numeric") + + if numeric != nil { + t.Fatal(numeric) + } +} + +func TestMax(t *testing.T) { + validation := New() + max := validation.Validate("Rachid", "max:2") + + if max != nil { + t.Fatal(max) + } +} + +func TestMin(t *testing.T) { + validation := New() + min := validation.Validate(51, "min:50") + + if min != nil { + t.Fatal(min) + } +} + +func TestBetween(t *testing.T) { + validation := New() + between := validation.Validate(0, "between:50.1,60|required") + + if between != nil { + t.Fatal(between) + } +} + +func TestEmail(t *testing.T) { + validation := New() + email := validation.Validate("lafriakh.rachid@gmail.com", "email") + + if email != nil { + t.Fatal(email) + } +} diff --git a/view.go b/view.go new file mode 100644 index 0000000..14e82a3 --- /dev/null +++ b/view.go @@ -0,0 +1,182 @@ +package kira + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/Lafriakh/log" +) + +// View - struct for views logic. +type View struct { + App *App + Data map[string]interface{} + customPath string +} + +// Data - type of view data. +type Data map[string]interface{} + +// Data - to put data into views. +func (a *App) Data(data Data) { + a.View.Data = data +} + +// Render - short access to the view render method. +func (a *App) Render(w http.ResponseWriter, req *http.Request, templates ...string) { + a.View.Render(w, req, templates...) +} + +// AddPath to add a custom path to the view struct. +func (v *View) AddPath(path string) error { + // check if this path exists + if _, err := os.Stat(path); os.IsNotExist(err) { + return err + } + + v.customPath = path + + return nil +} + +// Render template +func (v *View) Render(w http.ResponseWriter, req *http.Request, templates ...string) { + // this will be used to catch the error + buf := &bytes.Buffer{} + + // hold all templates + var templatesFiles []string + baseTemplate := filepath.Base(templates[0]) + v.App.Configs.GetString("VIEWS_FILE_SUFFIX") + // loop throw all templates + for _, temp := range templates { + if _, err := os.Stat(filepath.Join(v.customPath, temp+v.App.Configs.GetString("VIEWS_FILE_SUFFIX"))); err == nil { + templatesFiles = append(templatesFiles, filepath.Join(v.customPath, temp+v.App.Configs.GetString("VIEWS_FILE_SUFFIX"))) + } else { + templatesFiles = append(templatesFiles, v.App.Configs.GetString("VIEWS_PATH")+temp+v.App.Configs.GetString("VIEWS_FILE_SUFFIX")) + } + } + + // parse templates + Template, err := template.New(baseTemplate).Funcs(v.FuncMap(req)).ParseFiles(templatesFiles...) + if err != nil { + log.Panic(err) + } + + // global variables + v.Global(req) + err = Template.Execute(buf, v.Data) + // reset view data before any error + v.Data = map[string]interface{}{} + + // check for errors + if err != nil { + v.App.Log.Panic(err) + } + + // write the response + buf.WriteTo(w) +} + +// Global ... +func (v *View) Global(req *http.Request) { + errors := &ViewErrors{} + errors.Errors = v.App.Session.GetWithDefault("errors", map[string]string{}).(map[string]string) + + // site + v.Data["site_name"] = v.App.Configs.GetString("SITE_NAME") + // request id + v.Data["request_id"] = req.Context().Value(v.App.Configs.GetString("SERVER_REQUEST_ID")) + // errors + v.Data["errors"] = errors + // csrf + v.Data["csrf"] = req.Context().Value("csrf") + v.Data["csrfField"] = template.HTML(fmt.Sprintf(``, v.App.Configs.GetString("CSRF_FIELD_NAME"), req.Context().Value("csrf"))) +} + +// FuncMap - return a collection of teplate functions. +func (v *View) FuncMap(req *http.Request) template.FuncMap { + return template.FuncMap{ + "session": func(key interface{}) interface{} { + return v.App.Session.Get(key) + }, + "config": func(key string) interface{} { + return v.App.Configs.Get(key) + }, + "include": func(filename string) interface{} { + // read the template content. + b, err := ioutil.ReadFile(v.App.Configs.GetString("VIEWS_PATH") + filename + v.App.Configs.GetString("VIEWS_FILE_SUFFIX")) + if err != nil { + return nil + } + // create a buffer. + var buffer bytes.Buffer + // parse the template. + tmpl, err := template.New("").Funcs(v.FuncMap(req)).Parse(string(b)) + if err != nil { + log.Panic(err) + } + // add global variables to the included template + v.Global(req) + + // execute the template with the data. + errs := tmpl.Execute(&buffer, v.Data) + // check for errors + if errs != nil { + log.Panic(errs) + } + + // return the template to the parent template. + return template.HTML(buffer.String()) + }, + "url": func() string { + return req.URL.Path + }, + "join": func(s ...string) string { + // first arg is sep, remaining args are strings to join + return strings.Join(s[1:], s[0]) + }, + } +} + +// JSON response. +func (v *View) JSON(w http.ResponseWriter, data interface{}) { + // parse data to json format + response, err := json.Marshal(data) + if err != nil { + log.Panic(err) + } + + // return json with headers... + w.Header().Set("Content-Type", "application/json") + w.Write(response) +} + +// ViewErrors - to manage view errors. +type ViewErrors struct { + Errors map[string]string +} + +// New - append errors to the struct. +func (e *ViewErrors) New(data map[string]string) { + e.Errors = data +} + +// All - return all errors. +func (e *ViewErrors) All() map[string]string { + return e.Errors +} + +// Has - check if the key exists, if exists return it's value. +func (e *ViewErrors) Has(key string) string { + if _, ok := e.Errors[key]; ok { + return e.Errors[key] + } + return "" +}