From fcff62d5b4fca1c083655ced2a06287670d4959e Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Sat, 2 Jun 2018 07:28:40 +0300 Subject: [PATCH] Some minor but helpful additions, like `CookieOption`. Relative: https://github.com/kataras/iris/issues/1018. Simple cookies example added too. Cookie encoding (side by side with the already session's cookie id encoding) and version upgrade will come tomorrow with a new HISTORY.md entry as well, stay tuned! Former-commit-id: d14181fac998d32d77690b1b3e42b6c7c72f1ace --- _examples/README.md | 4 + _examples/README_ZH.md | 4 + _examples/cookies/basic/main.go | 64 ++++++++++ _examples/cookies/basic/main_test.go | 32 +++++ context/context.go | 170 ++++++++++++++++++++++----- go19.go | 10 ++ httptest/httptest.go | 1 + iris.go | 22 ++++ 8 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 _examples/cookies/basic/main.go create mode 100644 _examples/cookies/basic/main_test.go diff --git a/_examples/README.md b/_examples/README.md index 1dfa6953..f16c0e9a 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -395,6 +395,10 @@ iris cache library lives on its own [package](https://github.com/kataras/iris/tr > You're free to use your own favourite caching package if you'd like so. +### Cookies + +- [Basic](cookies/basic/main.go) + ### Sessions iris session manager lives on its own [package](https://github.com/kataras/iris/tree/master/sessions). diff --git a/_examples/README_ZH.md b/_examples/README_ZH.md index 946f9a33..66c36ca1 100644 --- a/_examples/README_ZH.md +++ b/_examples/README_ZH.md @@ -394,6 +394,10 @@ Iris 独立缓存包 [package](https://github.com/kataras/iris/tree/master/cache > 可以随意使用自定义的缓存包。 +### Cookies + +- [Basic](cookies/basic/main.go) + ### Sessions Iris session 管理独立包 [package](https://github.com/kataras/iris/tree/master/sessions). diff --git a/_examples/cookies/basic/main.go b/_examples/cookies/basic/main.go new file mode 100644 index 00000000..9b3eee42 --- /dev/null +++ b/_examples/cookies/basic/main.go @@ -0,0 +1,64 @@ +package main + +import "github.com/kataras/iris" + +func newApp() *iris.Application { + app := iris.New() + + // Set A Cookie. + app.Get("/cookies/{name}/{value}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + value := ctx.Params().Get("value") + + ctx.SetCookieKV(name, value) // <-- + // Alternatively: ctx.SetCookie(&http.Cookie{...}) + // + // If you want to set custom the path: + // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) + // + // If you want to be visible only to current request path: + // (note that client should be responsible for that if server sent an empty cookie's path, all browsers are compatible) + // ctx.SetCookieKV(name, value, iris.CookieCleanPath /* or iris.CookiePath("") */) + // More: + // iris.CookieExpires(time.Duration) + // iris.CookieHTTPOnly(false) + + ctx.Writef("cookie added: %s = %s", name, value) + }) + + // Retrieve A Cookie. + app.Get("/cookies/{name}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + + value := ctx.GetCookie(name) // <-- + // If you want more than the value then: + // cookie, err := ctx.Request().Cookie(name) + // if err != nil { + // handle error. + // } + + ctx.WriteString(value) + }) + + // Delete A Cookie. + app.Delete("/cookies/{name}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + + ctx.RemoveCookie(name) // <-- + // If you want to set custom the path: + // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) + + ctx.Writef("cookie %s removed", name) + }) + + return app +} + +func main() { + app := newApp() + + // GET: http://localhost:8080/cookies/my_name/my_value + // GET: http://localhost:8080/cookies/my_name + // DELETE: http://localhost:8080/cookies/my_name + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/cookies/basic/main_test.go b/_examples/cookies/basic/main_test.go new file mode 100644 index 00000000..f2edd2e4 --- /dev/null +++ b/_examples/cookies/basic/main_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestCookiesBasic(t *testing.T) { + app := newApp() + e := httptest.New(t, app, httptest.URL("http://example.com")) + + cookieName, cookieValue := "my_cookie_name", "my_cookie_value" + + // Test Set A Cookie. + t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) + t1.Cookie(cookieName).Value().Equal(cookieValue) // validate cookie's existence, it should be there now. + t1.Body().Contains(cookieValue) + + // Test Retrieve A Cookie. + t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) + t2.Body().Equal(cookieValue) + + // Test Remove A Cookie. + t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) + t3.Body().Contains(cookieName) + + t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) + t4.Cookies().Empty() + t4.Body().Empty() +} diff --git a/context/context.go b/context/context.go index e9bb8454..e4c70d73 100644 --- a/context/context.go +++ b/context/context.go @@ -854,18 +854,41 @@ type Context interface { // | Cookies | // +------------------------------------------------------------+ - // SetCookie adds a cookie - SetCookie(cookie *http.Cookie) - // SetCookieKV adds a cookie, receives just a name(string) and a value(string) + // SetCookie adds a cookie. + // Use of the "options" is not required, they can be used to amend the "cookie". // - // If you use this method, it expires at 2 hours - // use ctx.SetCookie or http.SetCookie if you want to change more fields. - SetCookieKV(name, value string) + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic + SetCookie(cookie *http.Cookie, options ...CookieOption) + // SetCookieKV adds a cookie, requires the name(string) and the value(string). + // + // By default it expires at 2 hours and it's added to the root path, + // use the `CookieExpires` and `CookiePath` to modify them. + // Alternatively: ctx.SetCookie(&http.Cookie{...}) + // + // If you want to set custom the path: + // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) + // + // If you want to be visible only to current request path: + // ctx.SetCookieKV(name, value, iris.CookieCleanPath/iris.CookiePath("")) + // More: + // iris.CookieExpires(time.Duration) + // iris.CookieHTTPOnly(false) + // + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic + SetCookieKV(name, value string, options ...CookieOption) // GetCookie returns cookie's value by it's name // returns empty string if nothing was found. - GetCookie(name string) string - // RemoveCookie deletes a cookie by it's name. - RemoveCookie(name string) + // + // If you want more than the value then: + // cookie, err := ctx.Request().Cookie("name") + // + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic + GetCookie(name string, options ...CookieOption) string + // RemoveCookie deletes a cookie by it's name and path = "/". + // Tip: change the cookie's path to the current one by: RemoveCookie("name", iris.CookieCleanPath) + // + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic + RemoveCookie(name string, options ...CookieOption) // VisitAllCookies takes a visitor which loops // on each (request's) cookies' name and value. VisitAllCookies(visitor func(name string, value string)) @@ -1233,7 +1256,7 @@ func (ctx *context) HandlerName() string { // It can be changed to a customized one if needed (very advanced usage). // // See `DefaultNext` for more information about this and why it's exported like this. -var Next = DefaultNext ///TODO: add an example for this usecase, i.e describe handlers and skip only file handlers. +var Next = DefaultNext // DefaultNext is the default function that executed on each middleware if `ctx.Next()` // is called. @@ -2992,57 +3015,144 @@ func (ctx *context) SendFile(filename string, destinationName string) error { } // +------------------------------------------------------------+ -// | Cookies, Session and Flashes | +// | Cookies | // +------------------------------------------------------------+ -// SetCookie adds a cookie -func (ctx *context) SetCookie(cookie *http.Cookie) { +// CookieOption is the type of function that is accepted on +// context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie` +// as their (last) variadic input argument to amend the end cookie's form. +// +// Any custom or built'n `CookieOption` is valid, +// see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more. +type CookieOption func(*http.Cookie) + +// CookiePath is a `CookieOption`. +// Use it to change the cookie's Path field. +func CookiePath(path string) CookieOption { + return func(c *http.Cookie) { + c.Path = path + } +} + +// CookieCleanPath is a `CookieOption`. +// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`. +func CookieCleanPath(c *http.Cookie) { + c.Path = "" +} + +// CookieExpires is a `CookieOption`. +// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie. +func CookieExpires(durFromNow time.Duration) CookieOption { + return func(c *http.Cookie) { + c.Expires = time.Now().Add(durFromNow) + c.MaxAge = int(durFromNow.Seconds()) + } +} + +// CookieHTTPOnly is a `CookieOption`. +// Use it to set the cookie's HttpOnly field to false or true. +// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. +func CookieHTTPOnly(httpOnly bool) CookieOption { + return func(c *http.Cookie) { + c.HttpOnly = httpOnly + } +} + +// SetCookie adds a cookie. +// Use of the "options" is not required, they can be used to amend the "cookie". +// +// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic +func (ctx *context) SetCookie(cookie *http.Cookie, options ...CookieOption) { + for _, opt := range options { + opt(cookie) + } + http.SetCookie(ctx.writer, cookie) } -var ( - // SetCookieKVExpiration is 2 hours by-default - // you can change it or simple, use the SetCookie for more control. - SetCookieKVExpiration = time.Duration(120) * time.Minute -) - -// SetCookieKV adds a cookie, receives just a name(string) and a value(string) +// SetCookieKV adds a cookie, requires the name(string) and the value(string). // -// If you use this method, it expires at 2 hours -// use ctx.SetCookie or http.SetCookie if you want to change more fields. -func (ctx *context) SetCookieKV(name, value string) { +// By default it expires at 2 hours and it's added to the root path, +// use the `CookieExpires` and `CookiePath` to modify them. +// Alternatively: ctx.SetCookie(&http.Cookie{...}) +// +// If you want to set custom the path: +// ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) +// +// If you want to be visible only to current request path: +// (note that client should be responsible for that if server sent an empty cookie's path, all browsers are compatible) +// ctx.SetCookieKV(name, value, iris.CookieCleanPath/iris.CookiePath("")) +// More: +// iris.CookieExpires(time.Duration) +// iris.CookieHTTPOnly(false) +// +// Examples: https://github.com/kataras/iris/tree/master/_examples/cookies/basic +func (ctx *context) SetCookieKV(name, value string, options ...CookieOption) { c := &http.Cookie{} + c.Path = "/" c.Name = name c.Value = url.QueryEscape(value) c.HttpOnly = true c.Expires = time.Now().Add(SetCookieKVExpiration) c.MaxAge = int(SetCookieKVExpiration.Seconds()) - ctx.SetCookie(c) + ctx.SetCookie(c, options...) } // GetCookie returns cookie's value by it's name // returns empty string if nothing was found. -func (ctx *context) GetCookie(name string) string { +// +// If you want more than the value then: +// cookie, err := ctx.Request().Cookie("name") +// +// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic +func (ctx *context) GetCookie(name string, options ...CookieOption) string { cookie, err := ctx.request.Cookie(name) if err != nil { return "" } + + // TODO: + // Q: Why named as `CookieOption` and not like `CookieInterceptor`? + // A: Because an interceptor would be able to modify the cookie AND stop the 'x' operation, but we don't want to cancel anything. + // + // Q: Why "Cookie Options" here? + // A: Because of the future suport of cookie encoding like I did with sessions. + // Two impl ideas: + // - Do it so each caller of `GetCookie/SetCookieKV/SetCookie` can have each own encoding or share one, no limit. + // - Do it so every of the above three methods will use the same encoding, therefore to the Application's level, limit per Iris app. + // We'll see... + // + // Finally, I should not forget to add links for the new translated READMEs(2) and push a new version with the minor changes so far, + // API is stable, so relax and do it on the next commit tomorrow, need sleep. + for _, opt := range options { + opt(cookie) + } + value, _ := url.QueryUnescape(cookie.Value) return value } -// RemoveCookie deletes a cookie by it's name. -func (ctx *context) RemoveCookie(name string) { +// SetCookieKVExpiration is 2 hours by-default +// you can change it or simple, use the SetCookie for more control. +// +// See `SetCookieKVExpiration` and `CookieExpires` for more. +var SetCookieKVExpiration = time.Duration(120) * time.Minute + +// RemoveCookie deletes a cookie by it's name and path = "/". +// Tip: change the cookie's path to the current one by: RemoveCookie("name", iris.CookieCleanPath) +// +// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic +func (ctx *context) RemoveCookie(name string, options ...CookieOption) { c := &http.Cookie{} c.Name = name c.Value = "" - c.Path = "/" + c.Path = "/" // if user wants to change it, use of the CookieOption `CookiePath` is required if not `ctx.SetCookie`. c.HttpOnly = true - // RFC says 1 second, but let's do it 1 minute to make sure is working + // RFC says 1 second, but let's do it 1 to make sure is working exp := time.Now().Add(-time.Duration(1) * time.Minute) c.Expires = exp c.MaxAge = -1 - ctx.SetCookie(c) + ctx.SetCookie(c, options...) // delete request's cookie also, which is temporary available. ctx.request.Header.Set("Cookie", "") } diff --git a/go19.go b/go19.go index 0290ce2f..dd196196 100644 --- a/go19.go +++ b/go19.go @@ -71,4 +71,14 @@ type ( // // See `ExecutionRules` and `core/router/Party#SetExecutionRules` for more. ExecutionOptions = router.ExecutionOptions + + // CookieOption is the type of function that is accepted on + // context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie` + // as their (last) variadic input argument to amend the end cookie's form. + // + // Any custom or built'n `CookieOption` is valid, + // see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more. + // + // An alias for the `context/Context#CookieOption`. + CookieOption = context.CookieOption ) diff --git a/httptest/httptest.go b/httptest/httptest.go index 59c59cc2..b37f9750 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -87,6 +87,7 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe // set the logger or disable it (default) and disable the updater (for any case). app.Configure(iris.WithoutVersionChecker) app.Logger().SetLevel(conf.LogLevel) + if err := app.Build(); err != nil { if conf.Debug && (conf.LogLevel == "disable" || conf.LogLevel == "disabled") { app.Logger().Println(err.Error()) diff --git a/iris.go b/iris.go index 5bbcafff..a7624329 100644 --- a/iris.go +++ b/iris.go @@ -422,6 +422,28 @@ var ( // // A shortcut of the `cache#Cache304`. Cache304 = cache.Cache304 + + // CookiePath is a `CookieOption`. + // Use it to change the cookie's Path field. + // + // A shortcut for the `context#CookiePath`. + CookiePath = context.CookiePath + // CookieCleanPath is a `CookieOption`. + // Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`. + // + // A shortcut for the `context#CookieCleanPath`. + CookieCleanPath = context.CookieCleanPath + // CookieExpires is a `CookieOption`. + // Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie. + // + // A shortcut for the `context#CookieExpires`. + CookieExpires = context.CookieExpires + // CookieHTTPOnly is a `CookieOption`. + // Use it to set the cookie's HttpOnly field to false or true. + // HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. + // + // A shortcut for the `context#CookieHTTPOnly`. + CookieHTTPOnly = context.CookieHTTPOnly ) // SPA accepts an "assetHandler" which can be the result of an