diff --git a/_examples/README.md b/_examples/README.md index f16c0e9a..38f81384 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -398,6 +398,7 @@ iris cache library lives on its own [package](https://github.com/kataras/iris/tr ### Cookies - [Basic](cookies/basic/main.go) +- [Encode/Decode (securecookie)](cookies/securecookie/main.go) ### Sessions diff --git a/_examples/README_ZH.md b/_examples/README_ZH.md index 66c36ca1..f8ada68b 100644 --- a/_examples/README_ZH.md +++ b/_examples/README_ZH.md @@ -397,6 +397,7 @@ Iris 独立缓存包 [package](https://github.com/kataras/iris/tree/master/cache ### Cookies - [Basic](cookies/basic/main.go) +- [Encode/Decode (securecookie)](cookies/securecookie/main.go) ### Sessions diff --git a/_examples/cookies/securecookie/main.go b/_examples/cookies/securecookie/main.go new file mode 100644 index 00000000..9e5dd5cd --- /dev/null +++ b/_examples/cookies/securecookie/main.go @@ -0,0 +1,59 @@ +package main + +// developers can use any library to add a custom cookie encoder/decoder. +// At this example we use the gorilla's securecookie package: +// $ go get github.com/gorilla/securecookie +// $ go run main.go + +import ( + "github.com/kataras/iris" + + "github.com/gorilla/securecookie" +) + +var ( + // AES only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + hashKey = []byte("the-big-and-secret-fash-key-here") + blockKey = []byte("lot-secret-of-characters-big-too") + sc = securecookie.New(hashKey, blockKey) +) + +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, iris.CookieEncode(sc.Encode)) // <-- + + 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, iris.CookieDecode(sc.Decode)) // <-- + + ctx.WriteString(value) + }) + + // Delete A Cookie. + app.Delete("/cookies/{name}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + + ctx.RemoveCookie(name) // <-- + + ctx.Writef("cookie %s removed", name) + }) + + return app +} + +func main() { + app := newApp() + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/cookies/securecookie/main_test.go b/_examples/cookies/securecookie/main_test.go new file mode 100644 index 00000000..1298e310 --- /dev/null +++ b/_examples/cookies/securecookie/main_test.go @@ -0,0 +1,34 @@ +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) + // note that this will not work because it doesn't always returns the same value: + // cookieValueEncoded, _ := sc.Encode(cookieName, cookieValue) + t1.Cookie(cookieName).Value().NotEqual(cookieValue) // validate cookie's existence and value is not on its raw form. + 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/_examples/routing/README.md b/_examples/routing/README.md index 6bf32e2a..62cde33e 100644 --- a/_examples/routing/README.md +++ b/_examples/routing/README.md @@ -676,6 +676,14 @@ type Context interface { // was being registered to this request's path. GetCurrentRoute() RouteReadOnly + // Do calls the SetHandlers(handlers) + // and executes the first handler, + // handlers should not be empty. + // + // It's used by the router, developers may use that + // to replace and execute handlers immediately. + Do(Handlers) + // AddHandler can add handler(s) // to the current request in serve-time, // these handlers are not persistenced to the router. @@ -697,6 +705,43 @@ type Context interface { // // Look Handlers(), Next() and StopExecution() too. HandlerIndex(n int) (currentIndex int) + // Proceed is an alternative way to check if a particular handler + // has been executed and called the `ctx.Next` function inside it. + // This is useful only when you run a handler inside + // another handler. It justs checks for before index and the after index. + // + // A usecase example is when you want to execute a middleware + // inside controller's `BeginRequest` that calls the `ctx.Next` inside it. + // The Controller looks the whole flow (BeginRequest, method handler, EndRequest) + // as one handler, so `ctx.Next` will not be reflected to the method handler + // if called from the `BeginRequest`. + // + // Although `BeginRequest` should NOT be used to call other handlers, + // the `BeginRequest` has been introduced to be able to set + // common data to all method handlers before their execution. + // Controllers can accept middleware(s) from the MVC's Application's Router as normally. + // + // That said let's see an example of `ctx.Proceed`: + // + // var authMiddleware = basicauth.New(basicauth.Config{ + // Users: map[string]string{ + // "admin": "password", + // }, + // }) + // + // func (c *UsersController) BeginRequest(ctx iris.Context) { + // if !ctx.Proceed(authMiddleware) { + // ctx.StopExecution() + // } + // } + // This Get() will be executed in the same handler as `BeginRequest`, + // internally controller checks for `ctx.StopExecution`. + // So it will not be fired if BeginRequest called the `StopExecution`. + // func(c *UsersController) Get() []models.User { + // return c.Service.GetAll() + //} + // Alternative way is `!ctx.IsStopped()` if middleware make use of the `ctx.StopExecution()` on failure. + Proceed(Handler) bool // HandlerName returns the current handler's name, helpful for debugging. HandlerName() string // Next calls all the next handler from the handlers chain, @@ -704,14 +749,29 @@ type Context interface { // // Note: Custom context should override this method in order to be able to pass its own context.Context implementation. Next() - // NextHandler returns(but it is NOT executes) the next handler from the handlers chain. + // NextOr checks if chain has a next handler, if so then it executes it + // otherwise it sets a new chain assigned to this Context based on the given handler(s) + // and executes its first handler. + // + // Returns true if next handler exists and executed, otherwise false. + // + // Note that if no next handler found and handlers are missing then + // it sends a Status Not Found (404) to the client and it stops the execution. + NextOr(handlers ...Handler) bool + // NextOrNotFound checks if chain has a next handler, if so then it executes it + // otherwise it sends a Status Not Found (404) to the client and stops the execution. + // + // Returns true if next handler exists and executed, otherwise false. + NextOrNotFound() bool + // NextHandler returns (it doesn't execute) the next handler from the handlers chain. // // Use .Skip() to skip this handler if needed to execute the next of this returning handler. NextHandler() Handler // Skip skips/ignores the next handler from the handlers chain, // it should be used inside a middleware. Skip() - // StopExecution if called then the following .Next calls are ignored. + // StopExecution if called then the following .Next calls are ignored, + // as a result the next handlers in the chain will not be fire. StopExecution() // IsStopped checks and returns true if the current position of the Context is 255, // means that the StopExecution() was called. @@ -759,6 +819,8 @@ type Context interface { // Subdomain returns the subdomain of this request, if any. // Note that this is a fast method which does not cover all cases. Subdomain() (subdomain string) + // IsWWW returns true if the current subdomain (if any) is www. + IsWWW() bool // RemoteAddr tries to parse and return the real client's request IP. // // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. @@ -787,7 +849,12 @@ type Context interface { // Read more at: https://developer.mozilla.org/en-US/docs/AJAX // and https://xhr.spec.whatwg.org/ IsAjax() bool - + // IsMobile checks if client is using a mobile device(phone or tablet) to communicate with this server. + // If the return value is true that means that the http client using a mobile + // device to communicate with the server, otherwise false. + // + // Keep note that this checks the "User-Agent" request header. + IsMobile() bool // +------------------------------------------------------------+ // | Response Headers helpers | // +------------------------------------------------------------+ @@ -801,6 +868,10 @@ type Context interface { // which may, setted before with the 'ContentType'. GetContentType() string + // GetContentLength returns the request's header value of "Content-Length". + // Returns 0 if header was unable to be found or its value was not a valid number. + GetContentLength() int64 + // StatusCode sets the status code header to the response. // Look .GetStatusCode too. StatusCode(statusCode int) @@ -808,44 +879,160 @@ type Context interface { // Look StatusCode too. GetStatusCode() int - // Redirect redirect sends a redirect response the client + // Redirect sends a redirect response to the client + // to a specific url or relative path. // accepts 2 parameters string and an optional int // first parameter is the url to redirect - // second parameter is the http status should send, default is 302 (StatusFound), - // you can set it to 301 (Permant redirect), if that's nessecery + // second parameter is the http status should send, + // default is 302 (StatusFound), + // you can set it to 301 (Permant redirect) + // or 303 (StatusSeeOther) if POST method, + // or StatusTemporaryRedirect(307) if that's nessecery. Redirect(urlToRedirect string, statusHeader ...int) // +------------------------------------------------------------+ // | Various Request and Post Data | // +------------------------------------------------------------+ - // URLParam returns the get parameter from a request , if any. + // URLParam returns true if the url parameter exists, otherwise false. + URLParamExists(name string) bool + // URLParamDefault returns the get parameter from a request, + // if not found then "def" is returned. + URLParamDefault(name string, def string) string + // URLParam returns the get parameter from a request, if any. URLParam(name string) string + // URLParamTrim returns the url query parameter with trailing white spaces removed from a request. + URLParamTrim(name string) string + // URLParamTrim returns the escaped url query parameter from a request. + URLParamEscape(name string) string // URLParamInt returns the url query parameter as int value from a request, - // returns an error if parse failed. + // returns -1 and an error if parse failed. URLParamInt(name string) (int, error) + // URLParamIntDefault returns the url query parameter as int value from a request, + // if not found or parse failed then "def" is returned. + URLParamIntDefault(name string, def int) int // URLParamInt64 returns the url query parameter as int64 value from a request, - // returns an error if parse failed. + // returns -1 and an error if parse failed. URLParamInt64(name string) (int64, error) + // URLParamInt64Default returns the url query parameter as int64 value from a request, + // if not found or parse failed then "def" is returned. + URLParamInt64Default(name string, def int64) int64 + // URLParamFloat64 returns the url query parameter as float64 value from a request, + // returns -1 and an error if parse failed. + URLParamFloat64(name string) (float64, error) + // URLParamFloat64Default returns the url query parameter as float64 value from a request, + // if not found or parse failed then "def" is returned. + URLParamFloat64Default(name string, def float64) float64 + // URLParamBool returns the url query parameter as boolean value from a request, + // returns an error if parse failed or not found. + URLParamBool(name string) (bool, error) // URLParams returns a map of GET query parameters separated by comma if more than one // it returns an empty map if nothing found. URLParams() map[string]string - // FormValue returns a single form value by its name/key + // FormValueDefault returns a single parsed form value by its "name", + // including both the URL field's query parameters and the POST or PUT form data. + // + // Returns the "def" if not found. + FormValueDefault(name string, def string) string + // FormValue returns a single parsed form value by its "name", + // including both the URL field's query parameters and the POST or PUT form data. FormValue(name string) string - // FormValues returns all post data values with their keys - // form data, get, post & put query arguments + // FormValues returns the parsed form data, including both the URL + // field's query parameters and the POST or PUT form data. + // + // The default form's memory maximum size is 32MB, it can be changed by the + // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. // // NOTE: A check for nil is necessary. FormValues() map[string][]string - // PostValue returns a form's only-post value by its name, - // same as Request.PostFormValue. - PostValue(name string) string - // FormFile returns the first file for the provided form key. - // FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary. + + // PostValueDefault returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name". // - // same as Request.FormFile. + // If not found then "def" is returned instead. + PostValueDefault(name string, def string) string + // PostValue returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name" + PostValue(name string) string + // PostValueTrim returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", without trailing spaces. + PostValueTrim(name string) string + // PostValueInt returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as int. + // + // If not found returns -1 and a non-nil error. + PostValueInt(name string) (int, error) + // PostValueIntDefault returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as int. + // + // If not found returns or parse errors the "def". + PostValueIntDefault(name string, def int) int + // PostValueInt64 returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as float64. + // + // If not found returns -1 and a no-nil error. + PostValueInt64(name string) (int64, error) + // PostValueInt64Default returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as int64. + // + // If not found or parse errors returns the "def". + PostValueInt64Default(name string, def int64) int64 + // PostValueInt64Default returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as float64. + // + // If not found returns -1 and a non-nil error. + PostValueFloat64(name string) (float64, error) + // PostValueInt64Default returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as float64. + // + // If not found or parse errors returns the "def". + PostValueFloat64Default(name string, def float64) float64 + // PostValueInt64Default returns the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name", as bool. + // + // If not found or value is false, then it returns false, otherwise true. + PostValueBool(name string) (bool, error) + // PostValues returns all the parsed form data from POST, PATCH, + // or PUT body parameters based on a "name" as a string slice. + // + // The default form's memory maximum size is 32MB, it can be changed by the + // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. + PostValues(name string) []string + // FormFile returns the first uploaded file that received from the client. + // + // The default form's memory maximum size is 32MB, it can be changed by the + // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/http_request/upload-file FormFile(key string) (multipart.File, *multipart.FileHeader, error) + // UploadFormFiles uploads any received file(s) from the client + // to the system physical location "destDirectory". + // + // The second optional argument "before" gives caller the chance to + // modify the *miltipart.FileHeader before saving to the disk, + // it can be used to change a file's name based on the current request, + // all FileHeader's options can be changed. You can ignore it if + // you don't need to use this capability before saving a file to the disk. + // + // Note that it doesn't check if request body streamed. + // + // Returns the copied length as int64 and + // a not nil error if at least one new file + // can't be created due to the operating system's permissions or + // http.ErrMissingFile if no file received. + // + // If you want to receive & accept files and manage them manually you can use the `context#FormFile` + // instead and create a copy function that suits your needs, the below is for generic usage. + // + // The default form's memory maximum size is 32MB, it can be changed by the + // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. + // + // See `FormFile` to a more controlled to receive a file. + // + // + // Example: https://github.com/kataras/iris/tree/master/_examples/http_request/upload-files + UploadFormFiles(destDirectory string, before ...func(Context, *multipart.FileHeader)) (n int64, err error) // +------------------------------------------------------------+ // | Custom HTTP Errors | @@ -867,16 +1054,24 @@ type Context interface { // should be called before reading the request body from the client. SetMaxRequestBodySize(limitOverBytes int64) - // UnmarshalBody reads the request's body and binds it to a value or pointer of any type + // UnmarshalBody reads the request's body and binds it to a value or pointer of any type. // Examples of usage: context.ReadJSON, context.ReadXML. - UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error - // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type. - ReadJSON(jsonObject interface{}) error - // ReadXML reads XML from request's body and binds it to a value of any xml-valid type. - ReadXML(xmlObject interface{}) error + // + // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-custom-via-unmarshaler/main.go + UnmarshalBody(outPtr interface{}, unmarshaler Unmarshaler) error + // ReadJSON reads JSON from request's body and binds it to a pointer of a value of any json-valid type. + // + // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-json/main.go + ReadJSON(jsonObjectPtr interface{}) error + // ReadXML reads XML from request's body and binds it to a pointer of a value of any xml-valid type. + // + // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go + ReadXML(xmlObjectPtr interface{}) error // ReadForm binds the formObject with the form data // it supports any kind of struct. - ReadForm(formObject interface{}) error + // + // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go + ReadForm(formObjectPtr interface{}) error // +------------------------------------------------------------+ // | Body (raw) Writers | @@ -910,6 +1105,39 @@ type Context interface { // // Returns the number of bytes written and any write error encountered. WriteString(body string) (int, error) + + // SetLastModified sets the "Last-Modified" based on the "modtime" input. + // If "modtime" is zero then it does nothing. + // + // It's mostly internally on core/router and context packages. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + SetLastModified(modtime time.Time) + // CheckIfModifiedSince checks if the response is modified since the "modtime". + // Note that it has nothing to do with server-side caching. + // It does those checks by checking if the "If-Modified-Since" request header + // sent by client or a previous server response header + // (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) + // is a valid one and it's before the "modtime". + // + // A check for !modtime && err == nil is necessary to make sure that + // it's not modified since, because it may return false but without even + // had the chance to check the client-side (request) header due to some errors, + // like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero + // or if parsing time from the header failed. + // + // It's mostly used internally, e.g. `context#WriteWithExpiration`. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + CheckIfModifiedSince(modtime time.Time) (bool, error) + // WriteNotModified sends a 304 "Not Modified" status code to the client, + // it makes sure that the content type, the content length headers + // and any "ETag" are removed before the response sent. + // + // It's mostly used internally on core/router/fs.go and context methods. + WriteNotModified() // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. WriteWithExpiration(body []byte, modtime time.Time) (int, error) @@ -936,13 +1164,11 @@ type Context interface { ClientSupportsGzip() bool // WriteGzip accepts bytes, which are compressed to gzip format and sent to the client. // returns the number of bytes written and an error ( if the client doesn' supports gzip compression) - // - // This function writes temporary gzip contents, the ResponseWriter is untouched. + // You may re-use this function in the same handler + // to write more data many times without any troubles. WriteGzip(b []byte) (int, error) // TryWriteGzip accepts bytes, which are compressed to gzip format and sent to the client. // If client does not supprots gzip then the contents are written as they are, uncompressed. - // - // This function writes temporary gzip contents, the ResponseWriter is untouched. TryWriteGzip(b []byte) (int, error) // GzipResponseWriter converts the current response writer into a response writer // which when its .Write called it compress the data to gzip and writes them to the client. @@ -970,7 +1196,6 @@ type Context interface { // // Example: https://github.com/kataras/iris/tree/master/_examples/view/context-view-data/ ViewLayout(layoutTmplFile string) - // ViewData saves one or more key-value pair in order to be passed if and when .View // is being called afterwards, in the same request. // Useful when need to set or/and change template data from previous hanadlers in the chain. @@ -990,7 +1215,6 @@ type Context interface { // // Example: https://github.com/kataras/iris/tree/master/_examples/view/context-view-data/ ViewData(key string, value interface{}) - // GetViewData returns the values registered by `context#ViewData`. // The return value is `map[string]interface{}`, this means that // if a custom struct registered to ViewData then this function @@ -1001,16 +1225,20 @@ type Context interface { // Similarly to `viewData := ctx.Values().Get("iris.viewData")` or // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`. GetViewData() map[string]interface{} - - // View renders templates based on the adapted view engines. - // First argument accepts the filename, relative to the view engine's Directory, + // View renders a template based on the registered view engine(s). + // First argument accepts the filename, relative to the view engine's Directory and Extension, // i.e: if directory is "./templates" and want to render the "./templates/users/index.html" // then you pass the "users/index.html" as the filename argument. // - // Look: .ViewData and .ViewLayout too. + // The second optional argument can receive a single "view model" + // that will be binded to the view template if it's not nil, + // otherwise it will check for previous view data stored by the `ViewData` + // even if stored at any previous handler(middleware) for the same request. // - // Examples: https://github.com/kataras/iris/tree/master/_examples/view/ - View(filename string) error + // Look .ViewData` and .ViewLayout too. + // + // Examples: https://github.com/kataras/iris/tree/master/_examples/view + View(filename string, optionalViewModel ...interface{}) error // Binary writes out the raw bytes as binary data. Binary(data []byte) (int, error) @@ -1024,9 +1252,10 @@ type Context interface { JSONP(v interface{}, options ...JSONP) (int, error) // XML marshals the given interface object and writes the XML response. XML(v interface{}, options ...XML) (int, error) - // Markdown parses the markdown to html and renders to client. + // Markdown parses the markdown to html and renders its result to the client. Markdown(markdownB []byte, options ...Markdown) (int, error) - + // YAML parses the "v" using the yaml parser and renders its result to the client. + YAML(v interface{}) (int, error) // +------------------------------------------------------------+ // | Serve files | // +------------------------------------------------------------+ @@ -1034,18 +1263,23 @@ type Context interface { // ServeContent serves content, headers are autoset // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) // - // You can define your own "Content-Type" header also, after this function call - // Doesn't implements resuming (by range), use ctx.SendFile instead + // + // You can define your own "Content-Type" with `context#ContentType`, before this function call. + // + // This function doesn't support resuming (by range), + // use ctx.SendFile or router's `StaticWeb` instead. ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error - // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) + // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) // receives two parameters // filename/path (string) // gzipCompression (bool) // - // You can define your own "Content-Type" header also, after this function call - // This function doesn't implement resuming (by range), use ctx.SendFile instead + // You can define your own "Content-Type" with `context#ContentType`, before this function call. // - // Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile. + // This function doesn't support resuming (by range), + // use ctx.SendFile or router's `StaticWeb` instead. + // + // Use it when you want to serve dynamic files to the client. ServeFile(filename string, gzipCompression bool) error // SendFile sends file for force-download to the client // @@ -1056,18 +1290,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)) @@ -1112,7 +1369,7 @@ type Context interface { // TransactionsSkipped returns true if the transactions skipped or canceled at all. TransactionsSkipped() bool - // Exec calls the framewrok's ServeCtx + // Exec calls the `context/Application#ServeCtx` // based on this context but with a changed method and path // like it was requested by the user, but it is not. // @@ -1135,7 +1392,11 @@ type Context interface { // Context's Values and the Session are kept in order to be able to communicate via the result route. // // It's for extreme use cases, 99% of the times will never be useful for you. - Exec(method string, path string) + Exec(method, path string) + + // RouteExists reports whether a particular route exists + // It will search from the current subdomain of context's host, if not inside the root domain. + RouteExists(method, path string) bool // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface @@ -1143,5 +1404,14 @@ type Context interface { // to be executed at serve-time. The full app's fields // and methods are not available here for the developer's safety. Application() Application + + // String returns the string representation of this request. + // Each context has a unique string representation. + // It can be used for simple debugging scenarios, i.e print context as string. + // + // What it returns? A number which declares the length of the + // total `String` calls per executable application, followed + // by the remote IP (the client) and finally the method:url. + String() string } ``` \ No newline at end of file diff --git a/context/context.go b/context/context.go index e4c70d73..3d09338d 100644 --- a/context/context.go +++ b/context/context.go @@ -3058,6 +3058,64 @@ func CookieHTTPOnly(httpOnly bool) CookieOption { } } +type ( + // CookieEncoder should encode the cookie value. + // Should accept as first argument the cookie name + // and as second argument the cookie value ptr. + // Should return an encoded value or an empty one if encode operation failed. + // Should return an error if encode operation failed. + // + // Note: Errors are not printed, so you have to know what you're doing, + // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + // + // See `CookieDecoder` too. + CookieEncoder func(cookieName string, value interface{}) (string, error) + // CookieDecoder should decode the cookie value. + // Should accept as first argument the cookie name, + // as second argument the encoded cookie value and as third argument the decoded value ptr. + // Should return a decoded value or an empty one if decode operation failed. + // Should return an error if decode operation failed. + // + // Note: Errors are not printed, so you have to know what you're doing, + // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + // + // See `CookieEncoder` too. + CookieDecoder func(cookieName string, cookieValue string, v interface{}) error +) + +// CookieEncode is a `CookieOption`. +// Provides encoding functionality when adding a cookie. +// Accepts a `CookieEncoder` and sets the cookie's value to the encoded value. +// Users of that is the `SetCookie` and `SetCookieKV`. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie +func CookieEncode(encode CookieEncoder) CookieOption { + return func(c *http.Cookie) { + newVal, err := encode(c.Name, c.Value) + if err != nil { + c.Value = "" + } else { + c.Value = newVal + } + } +} + +// CookieDecode is a `CookieOption`. +// Provides decoding functionality when retrieving a cookie. +// Accepts a `CookieDecoder` and sets the cookie's value to the decoded value before return by the `GetCookie`. +// User of that is the `GetCookie`. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie +func CookieDecode(decode CookieDecoder) CookieOption { + return func(c *http.Cookie) { + if err := decode(c.Name, c.Value, &c.Value); err != nil { + c.Value = "" + } + } +} + // SetCookie adds a cookie. // Use of the "options" is not required, they can be used to amend the "cookie". // @@ -3111,19 +3169,6 @@ func (ctx *context) GetCookie(name string, options ...CookieOption) string { 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) } diff --git a/iris.go b/iris.go index a7624329..f84accea 100644 --- a/iris.go +++ b/iris.go @@ -444,6 +444,24 @@ var ( // // A shortcut for the `context#CookieHTTPOnly`. CookieHTTPOnly = context.CookieHTTPOnly + // CookieEncode is a `CookieOption`. + // Provides encoding functionality when adding a cookie. + // Accepts a `context#CookieEncoder` and sets the cookie's value to the encoded value. + // Users of that is the `context#SetCookie` and `context#SetCookieKV`. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie + // + // A shortcut for the `context#CookieEncode`. + CookieEncode = context.CookieEncode + // CookieDecode is a `CookieOption`. + // Provides decoding functionality when retrieving a cookie. + // Accepts a `context#CookieDecoder` and sets the cookie's value to the decoded value before return by the `GetCookie`. + // User of that is the `context#GetCookie`. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie + // + // A shortcut for the `context#CookieDecode`. + CookieDecode = context.CookieDecode ) // SPA accepts an "assetHandler" which can be the result of an