Permalink
Find file
cdd5370 Jan 18, 2017
1455 lines (1277 sloc) 52.7 KB
package iris
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net"
"net/http"
"os"
"path"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/iris-contrib/formBinder"
"github.com/kataras/go-errors"
"github.com/kataras/go-fs"
"github.com/kataras/go-sessions"
)
const (
// ContentType represents the header["Content-Type"]
contentType = "Content-Type"
// ContentLength represents the header["Content-Length"]
contentLength = "Content-Length"
// contentEncodingHeader represents the header["Content-Encoding"]
contentEncodingHeader = "Content-Encoding"
// varyHeader represents the header "Vary"
varyHeader = "Vary"
// acceptEncodingHeader represents the header key & value "Accept-Encoding"
acceptEncodingHeader = "Accept-Encoding"
// ContentHTML is the string of text/html response headers
contentHTML = "text/html"
// ContentBinary header value for binary data.
contentBinary = "application/octet-stream"
// ContentJSON header value for JSON data.
contentJSON = "application/json"
// ContentJSONP header value for JSONP & Javascript data.
contentJSONP = "application/javascript"
// ContentJavascript header value for Javascript/JSONP
// conversional
contentJavascript = "application/javascript"
// ContentText header value for Text data.
contentText = "text/plain"
// ContentXML header value for XML data.
contentXML = "text/xml"
// contentMarkdown custom key/content type, the real is the text/html
contentMarkdown = "text/markdown"
// LastModified "Last-Modified"
lastModified = "Last-Modified"
// IfModifiedSince "If-Modified-Since"
ifModifiedSince = "If-Modified-Since"
// ContentDisposition "Content-Disposition"
contentDisposition = "Content-Disposition"
// CacheControl "Cache-Control"
cacheControl = "Cache-Control"
// stopExecutionPosition used inside the Context, is the number which shows us that the context's middleware manually stop the execution
stopExecutionPosition = 255
)
type (
requestValue struct {
key []byte
value interface{}
}
requestValues []requestValue
)
func (r *requestValues) Set(key string, value interface{}) {
args := *r
n := len(args)
for i := 0; i < n; i++ {
kv := &args[i]
if string(kv.key) == key {
kv.value = value
return
}
}
c := cap(args)
if c > n {
args = args[:n+1]
kv := &args[n]
kv.key = append(kv.key[:0], key...)
kv.value = value
*r = args
return
}
kv := requestValue{}
kv.key = append(kv.key[:0], key...)
kv.value = value
*r = append(args, kv)
}
func (r *requestValues) Get(key string) interface{} {
args := *r
n := len(args)
for i := 0; i < n; i++ {
kv := &args[i]
if string(kv.key) == key {
return kv.value
}
}
return nil
}
func (r *requestValues) Reset() {
*r = (*r)[:0]
}
type (
// Map is just a conversion for a map[string]interface{}
// should not be used inside Render when PongoEngine is used.
Map map[string]interface{}
// Context is resetting every time a request is coming to the server
// it is not good practice to use this object in goroutines, for these cases use the .Clone()
Context struct {
ResponseWriter // *responseWriter by default, when record is on then *ResponseRecorder
Request *http.Request
values requestValues
framework *Framework
//keep track all registered middleware (handlers)
Middleware Middleware // exported because is useful for debugging
session sessions.Session
// Pos is the position number of the Context, look .Next to understand
Pos int // exported because is useful for debugging
}
)
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------Handler(s) Execution------------------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// Do calls the first handler only, it's like Next with negative pos, used only on Router&MemoryRouter
func (ctx *Context) Do() {
ctx.Pos = 0
ctx.Middleware[0].Serve(ctx)
}
// Next calls all the next handler from the middleware stack, it used inside a middleware
func (ctx *Context) Next() {
//set position to the next
ctx.Pos++
//run the next
if ctx.Pos < len(ctx.Middleware) {
ctx.Middleware[ctx.Pos].Serve(ctx)
}
}
// StopExecution just sets the .pos to 255 in order to not move to the next middlewares(if any)
func (ctx *Context) StopExecution() {
ctx.Pos = stopExecutionPosition
}
// IsStopped checks and returns true if the current position of the Context is 255, means that the StopExecution has called
func (ctx *Context) IsStopped() bool {
return ctx.Pos == stopExecutionPosition
}
// GetHandlerName as requested returns the stack-name of the function which the Middleware is setted from
func (ctx *Context) GetHandlerName() string {
return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name()
}
// ExecRoute calls any route (mostly "offline" route) like it was requested by the user, but it is not.
// Offline means that the route is registered to the iris and have all features that a normal route has
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRouteAgainst(routeName, againstRequestPath string),
// iris.None(...) and iris.SetRouteOnline/SetRouteOffline
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRoute(r Route) *Context {
return ctx.ExecRouteAgainst(r, ctx.Path())
}
// ExecRouteAgainst calls any iris.Route against a 'virtually' request path
// like it was requested by the user, but it is not.
// Offline means that the route is registered to the iris and have all features that a normal route has
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRoute(routeName),
// iris.None(...) and iris.SetRouteOnline/SetRouteOffline
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context {
if r != nil {
context := &(*ctx)
context.Middleware = context.Middleware[0:0]
context.values.Reset()
tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain())
tree.entry.get(againstRequestPath, context)
if len(context.Middleware) > 0 {
context.Do()
return context
}
}
return nil
}
// Prioritize is a middleware which executes a route against this path
// when the request's Path has a prefix of the route's STATIC PART
// is not executing ExecRoute to determinate if it's valid, for performance reasons
// if this function is not enough for you and you want to test more than one parameterized path
// then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ }
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// if the route found then it executes that and don't continue to the next handler
// if not found then continue to the next handler
func Prioritize(r Route) HandlerFunc {
if r != nil {
return func(ctx *Context) {
reqPath := ctx.Path()
if strings.HasPrefix(reqPath, r.StaticPath()) {
newctx := ctx.ExecRouteAgainst(r, reqPath)
if newctx == nil { // route not found.
ctx.EmitError(StatusNotFound)
}
return
}
// execute the next handler if no prefix
// here look, the only error we catch is the 404,
// we can't go ctx.Next() and believe that the next handler will manage the error
// because it will not, we are not on the router.
ctx.Next()
}
}
return func(ctx *Context) { ctx.Next() }
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------Request URL, Method, IP & Headers getters---------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// Method returns the http request method
// same as *http.Request.Method
func (ctx *Context) Method() string {
return ctx.Request.Method
}
// Host returns the host part of the current url
func (ctx *Context) Host() string {
h := ctx.Request.URL.Host
if h == "" {
h = ctx.Request.Host
}
return h
}
// ServerHost returns the server host taken by *http.Request.Host
func (ctx *Context) ServerHost() string {
return ctx.Request.Host
}
// Subdomain returns the subdomain (string) of this request, if any
func (ctx *Context) Subdomain() (subdomain string) {
host := ctx.Host()
if index := strings.IndexByte(host, '.'); index > 0 {
subdomain = host[0:index]
}
return
}
// VirtualHostname returns the hostname that user registers, host path maybe differs from the real which is HostString, which taken from a net.listener
func (ctx *Context) VirtualHostname() string {
realhost := ctx.Host()
hostname := realhost
virtualhost := ctx.framework.mux.hostname
if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 {
hostname = hostname[0:portIdx]
}
if idxDotAnd := strings.LastIndexByte(hostname, '.'); idxDotAnd > 0 {
s := hostname[idxDotAnd:]
// means that we have the request's host mymachine.com or 127.0.0.1/0.0.0.0, but for the second option we will need to replace it with the hostname that the dev was registered
// this needed to parse correct the {{ url }} iris global template engine's function
if s == ".1" {
hostname = strings.Replace(hostname, "127.0.0.1", virtualhost, 1)
} else if s == ".0" {
hostname = strings.Replace(hostname, "0.0.0.0", virtualhost, 1)
}
//
} else {
hostname = strings.Replace(hostname, "localhost", virtualhost, 1)
}
return hostname
}
// Path returns the full escaped path as string
// for unescaped use: ctx.RequestCtx.RequestURI() or RequestPath(escape bool)
func (ctx *Context) Path() string {
return ctx.RequestPath(ctx.framework.Config.EnablePathEscape)
}
// RequestPath returns the requested path
func (ctx *Context) RequestPath(escape bool) string {
if escape {
// NOTE: for example:
// DecodeURI decodes %2F to '/'
// DecodeQuery decodes any %20 to whitespace
// here we choose to be query-decoded only
// and with context.ParamDecoded the user receives a URI decoded path parameter.
// see https://github.com/iris-contrib/examples/tree/master/named_parameters_pathescape
// and https://github.com/iris-contrib/examples/tree/master/pathescape
return DecodeQuery(ctx.Request.URL.EscapedPath())
}
return ctx.Request.URL.Path
}
// RemoteAddr tries to return the real client's request IP
func (ctx *Context) RemoteAddr() string {
header := ctx.RequestHeader("X-Real-Ip")
realIP := strings.TrimSpace(header)
if realIP != "" {
return realIP
}
realIP = ctx.RequestHeader("X-Forwarded-For")
idx := strings.IndexByte(realIP, ',')
if idx >= 0 {
realIP = realIP[0:idx]
}
realIP = strings.TrimSpace(realIP)
if realIP != "" {
return realIP
}
addr := strings.TrimSpace(ctx.Request.RemoteAddr)
if len(addr) == 0 {
return ""
}
// if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is
if ip, _, err := net.SplitHostPort(addr); err == nil {
return ip
}
return addr
}
// RequestHeader returns the request header's value
// accepts one parameter, the key of the header (string)
// returns string
func (ctx *Context) RequestHeader(k string) string {
return ctx.Request.Header.Get(k)
}
// IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest)
//
// Read more at: http://www.w3schools.com/ajax/
func (ctx *Context) IsAjax() bool {
return ctx.RequestHeader("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------GET & POST arguments------------------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// URLParam returns the get parameter from a request , if any
func (ctx *Context) URLParam(key string) string {
return ctx.Request.URL.Query().Get(key)
}
// URLParams returns a map of GET query parameters separated by comma if more than one
// it returns an empty map if nothing founds
func (ctx *Context) URLParams() map[string]string {
values := map[string]string{}
q := ctx.URLParamsAsMulti()
if q != nil {
for k, v := range q {
values[k] = strings.Join(v, ",")
}
}
return values
}
// URLParamsAsMulti returns a map of list contains the url get parameters
func (ctx *Context) URLParamsAsMulti() map[string][]string {
return ctx.Request.URL.Query()
}
// URLParamInt returns the url query parameter as int value from a request , returns error on parse fail
func (ctx *Context) URLParamInt(key string) (int, error) {
return strconv.Atoi(ctx.URLParam(key))
}
// URLParamInt64 returns the url query parameter as int64 value from a request , returns error on parse fail
func (ctx *Context) URLParamInt64(key string) (int64, error) {
return strconv.ParseInt(ctx.URLParam(key), 10, 64)
}
func (ctx *Context) askParseForm() error {
if ctx.Request.Form == nil {
if err := ctx.Request.ParseForm(); err != nil {
return err
}
}
return nil
}
// FormValues returns all post data values with their keys
// form data, get, post & put query arguments
//
// NOTE: A check for nil is necessary for zero results
func (ctx *Context) FormValues() map[string][]string {
// we skip the check of multipart form, takes too much memory, if user wants it can do manually now.
if err := ctx.askParseForm(); err != nil {
return nil
}
return ctx.Request.Form // nothing more to do, it's already contains both query and post & put args.
}
// FormValue returns a single form value by its name/key
func (ctx *Context) FormValue(name string) string {
return ctx.Request.FormValue(name)
}
// PostValue returns a form's only-post value by its name
// same as Request.PostFormValue
func (ctx *Context) PostValue(name string) string {
return ctx.Request.PostFormValue(name)
}
// FormFile returns the first file for the provided form key.
// FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary.
//
// same as Request.FormFile
func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
return ctx.Request.FormFile(key)
}
var (
errTemplateExecute = errors.New("Unable to execute a template. Trace: %s")
errReadBody = errors.New("While trying to read %s from the request body. Trace %s")
errServeContent = errors.New("While trying to serve content to the client. Trace %s")
)
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------Request Body Binders/Readers----------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// NOTE: No default max body size http package has some built'n protection for DoS attacks
// See iris.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813
// and https://github.com/golang/go/issues/2093#issuecomment-66057824
// LimitRequestBodySize is a middleware which sets a request body size limit for all next handlers
// should be registered before all other handlers
var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) HandlerFunc {
return func(ctx *Context) {
ctx.SetMaxRequestBodySize(maxRequestBodySizeBytes)
ctx.Next()
}
}
// SetMaxRequestBodySize sets a limit to the request body size
// should be called before reading the request body from the client
func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
ctx.Request.Body = http.MaxBytesReader(ctx.ResponseWriter, ctx.Request.Body, limitOverBytes)
}
// BodyDecoder is an interface which any struct can implement in order to customize the decode action
// from ReadJSON and ReadXML
//
// Trivial example of this could be:
// type User struct { Username string }
//
// func (u *User) Decode(data []byte) error {
// return json.Unmarshal(data, u)
// }
//
// the 'context.ReadJSON/ReadXML(&User{})' will call the User's
// Decode option to decode the request body
//
// Note: This is totally optionally, the default decoders
// for ReadJSON is the encoding/json and for ReadXML is the encoding/xml
type BodyDecoder interface {
Decode(data []byte) error
}
// Unmarshaler is the interface implemented by types that can unmarshal any raw data
// TIP INFO: Any v object which implements the BodyDecoder can be override the unmarshaler
type Unmarshaler interface {
Unmarshal(data []byte, v interface{}) error
}
// UnmarshalerFunc a shortcut for the Unmarshaler interface
//
// See 'Unmarshaler' and 'BodyDecoder' for more
type UnmarshalerFunc func(data []byte, v interface{}) error
// Unmarshal parses the X-encoded data and stores the result in the value pointed to by v.
// Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps,
// slices, and pointers as necessary.
func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
return u(data, v)
}
// UnmarshalBody reads the request's body and binds it to a value or pointer of any type
// Examples of usage: context.ReadJSON, context.ReadXML
func (ctx *Context) UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error {
if ctx.Request.Body == nil {
return errors.New("Empty body, please send request body!")
}
rawData, err := ioutil.ReadAll(ctx.Request.Body)
if err != nil {
return err
}
// check if the v contains its own decode
// in this case the v should be a pointer also,
// but this is up to the user's custom Decode implementation*
//
// See 'BodyDecoder' for more
if decoder, isDecoder := v.(BodyDecoder); isDecoder {
return decoder.Decode(rawData)
}
// check if v is already a pointer, if yes then pass as it's
if reflect.TypeOf(v).Kind() == reflect.Ptr {
return unmarshaler.Unmarshal(rawData, v)
}
// finally, if the v doesn't contains a self-body decoder and it's not a pointer
// use the custom unmarshaler to bind the body
return unmarshaler.Unmarshal(rawData, &v)
}
// ReadJSON reads JSON from request's body and binds it to a value of any json-valid type
func (ctx *Context) ReadJSON(jsonObject interface{}) error {
return ctx.UnmarshalBody(jsonObject, UnmarshalerFunc(json.Unmarshal))
}
// ReadXML reads XML from request's body and binds it to a value of any xml-valid type
func (ctx *Context) ReadXML(xmlObject interface{}) error {
return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal))
}
// ReadForm binds the formObject with the form data
// it supports any kind of struct
func (ctx *Context) ReadForm(formObject interface{}) error {
values := ctx.FormValues()
if values == nil {
return errors.New("An empty form passed on context.ReadForm")
}
return errReadBody.With(formBinder.Decode(values, formObject))
}
/* Response */
// SetContentType sets the response writer's header key 'Content-Type' to a given value(s)
func (ctx *Context) SetContentType(s string) {
ctx.ResponseWriter.Header().Set(contentType, s)
}
// SetHeader write to the response writer's header to a given key the given value
func (ctx *Context) SetHeader(k string, v string) {
ctx.ResponseWriter.Header().Add(k, v)
}
// SetStatusCode sets the status code header to the response
//
// same as .WriteHeader, iris takes cares of your status code seriously
func (ctx *Context) SetStatusCode(statusCode int) {
ctx.ResponseWriter.WriteHeader(statusCode)
}
// Redirect redirect sends a redirect response the client
// 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
func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) {
ctx.StopExecution()
httpStatus := StatusFound // a 'temporary-redirect-like' which works better than for our purpose
if statusHeader != nil && len(statusHeader) > 0 && statusHeader[0] > 0 {
httpStatus = statusHeader[0]
}
if urlToRedirect == ctx.Path() {
if ctx.framework.Config.IsDevelopment {
ctx.Log("Trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect)
}
}
http.Redirect(ctx.ResponseWriter, ctx.Request, urlToRedirect, httpStatus)
}
// RedirectTo does the same thing as Redirect but instead of receiving a uri or path it receives a route name
func (ctx *Context) RedirectTo(routeName string, args ...interface{}) {
s := ctx.framework.URL(routeName, args...)
if s != "" {
ctx.Redirect(s, StatusFound)
}
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------(Custom) Errors-----------------------------------------
// ----------------------Look iris.OnError/EmitError for more---------------------------
// -------------------------------------------------------------------------------------
// NotFound emits an error 404 to the client, using the custom http errors
// if no custom errors provided then it sends the default error message
func (ctx *Context) NotFound() {
ctx.framework.EmitError(StatusNotFound, ctx)
}
// Panic emits an error 500 to the client, using the custom http errors
// if no custom errors rpovided then it sends the default error message
func (ctx *Context) Panic() {
ctx.framework.EmitError(StatusInternalServerError, ctx)
}
// EmitError executes the custom error by the http status code passed to the function
func (ctx *Context) EmitError(statusCode int) {
ctx.framework.EmitError(statusCode, ctx)
ctx.StopExecution()
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -------------------------Context's gzip inline response writer ----------------------
// ---------------------Look template.go & iris.go for more options---------------------
// -------------------------------------------------------------------------------------
var (
errClientDoesNotSupportGzip = errors.New("Client doesn't supports gzip compression")
)
func (ctx *Context) clientAllowsGzip() bool {
if h := ctx.RequestHeader(acceptEncodingHeader); h != "" {
for _, v := range strings.Split(h, ";") {
if strings.Contains(v, "gzip") { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){
return true
}
}
}
return false
}
// 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)
func (ctx *Context) WriteGzip(b []byte) (int, error) {
if ctx.clientAllowsGzip() {
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter)
n, err := gzipWriter.Write(b)
fs.ReleaseGzipWriter(gzipWriter)
if err == nil {
ctx.SetHeader(contentEncodingHeader, "gzip")
} // else write the contents as it is? no let's create a new func for this
return n, err
}
return 0, errClientDoesNotSupportGzip
}
// 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.
func (ctx *Context) TryWriteGzip(b []byte) (int, error) {
n, err := ctx.WriteGzip(b)
if err != nil {
// check if the error came from gzip not allowed and not the writer itself
if _, ok := err.(*errors.Error); ok {
// client didn't supported gzip, write them uncompressed:
return ctx.ResponseWriter.Write(b)
}
}
return n, err
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------Render and powerful content negotiation-----------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// renderSerialized renders contents with a serializer with status OK which you can change using RenderWithStatus or ctx.SetStatusCode(iris.StatusCode)
func (ctx *Context) renderSerialized(contentType string, obj interface{}, options ...map[string]interface{}) error {
s := ctx.framework.serializers
finalResult, err := s.Serialize(contentType, obj, options...)
if err != nil {
return err
}
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = getGzipOption(gzipEnabled, options[0]) // located to the template.go below the RenderOptions
charset = getCharsetOption(charset, options[0])
}
ctype := contentType
if ctype == contentMarkdown { // remember the text/markdown is just a custom internal iris content type, which in reallity renders html
ctype = contentHTML
}
if ctype != contentBinary { // set the charset only on non-binary data
ctype += "; charset=" + charset
}
ctx.SetContentType(ctype)
if gzipEnabled {
ctx.TryWriteGzip(finalResult)
} else {
ctx.ResponseWriter.Write(finalResult)
}
ctx.SetStatusCode(StatusOK)
return nil
}
// RenderTemplateSource serves a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string
func (ctx *Context) RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error {
err := ctx.framework.templates.renderSource(ctx, src, binding, options...)
if err == nil {
ctx.SetStatusCode(status)
}
return err
}
// RenderWithStatus builds up the response from the specified template or a serialize engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engines
func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) (err error) {
if _, shouldFirstStatusCode := ctx.ResponseWriter.(*responseWriter); shouldFirstStatusCode {
ctx.SetStatusCode(status)
}
if strings.IndexByte(name, '.') > -1 { //we have template
err = ctx.framework.templates.renderFile(ctx, name, binding, options...)
} else {
err = ctx.renderSerialized(name, binding, options...)
}
// we don't care for the last one it will not be written more than one if we have the *responseWriter
///TODO:
// if we have ResponseRecorder order doesn't matters but I think the transactions have bugs , for now let's keep it here because it 'fixes' one of them...
ctx.SetStatusCode(status)
return
}
// Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists
// builds up the response from the specified template or a serialize engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engine
func (ctx *Context) Render(name string, binding interface{}, options ...map[string]interface{}) error {
errCode := ctx.ResponseWriter.StatusCode()
if errCode <= 0 {
errCode = StatusOK
}
return ctx.RenderWithStatus(errCode, name, binding, options...)
}
// MustRender same as .Render but returns 503 service unavailable http status with a (html) message if render failed
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engine
func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) {
if err := ctx.Render(name, binding, options...); err != nil {
ctx.HTML(StatusServiceUnavailable, fmt.Sprintf("<h2>Template: %s</h2><b>%s</b>", name, err.Error()))
if ctx.framework.Config.IsDevelopment {
ctx.framework.Logger.Printf("MustRender panics on template: %s.Trace: %s\n", name, err)
}
}
}
// TemplateString accepts a template filename, its context data and returns the result of the parsed template (string)
// if any error returns empty string
func (ctx *Context) TemplateString(name string, binding interface{}, options ...map[string]interface{}) string {
return ctx.framework.TemplateString(name, binding, options...)
}
// HTML writes html string with a http status
func (ctx *Context) HTML(status int, htmlContents string) {
if err := ctx.RenderWithStatus(status, contentHTML, htmlContents); err != nil {
// if no serialize engine found for text/html
ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset)
ctx.SetStatusCode(status)
ctx.WriteString(htmlContents)
}
}
// Data writes out the raw bytes as binary data.
func (ctx *Context) Data(status int, v []byte) error {
return ctx.RenderWithStatus(status, contentBinary, v)
}
// JSON marshals the given interface object and writes the JSON response.
func (ctx *Context) JSON(status int, v interface{}) error {
return ctx.RenderWithStatus(status, contentJSON, v)
}
// JSONP marshals the given interface object and writes the JSON response.
func (ctx *Context) JSONP(status int, callback string, v interface{}) error {
return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback})
}
// Text writes out a string as plain text.
func (ctx *Context) Text(status int, v string) error {
return ctx.RenderWithStatus(status, contentText, v)
}
// XML marshals the given interface object and writes the XML response.
func (ctx *Context) XML(status int, v interface{}) error {
return ctx.RenderWithStatus(status, contentXML, v)
}
// MarkdownString parses the (dynamic) markdown string and returns the converted html string
func (ctx *Context) MarkdownString(markdownText string) string {
return ctx.framework.SerializeToString(contentMarkdown, markdownText)
}
// Markdown parses and renders to the client a particular (dynamic) markdown string
// accepts two parameters
// first is the http status code
// second is the markdown string
func (ctx *Context) Markdown(status int, markdown string) {
ctx.HTML(status, ctx.MarkdownString(markdown))
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------Static content serve by context implementation-------------------
// --------------------Look iris.go for more useful Static web system methods-----------
// -------------------------------------------------------------------------------------
// staticCachePassed checks the IfModifiedSince header and
// returns true if (client-side) duration has expired
func (ctx *Context) staticCachePassed(modtime time.Time) bool {
if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) {
ctx.ResponseWriter.Header().Del(contentType)
ctx.ResponseWriter.Header().Del(contentLength)
ctx.SetStatusCode(StatusNotModified)
return true
}
return false
}
// SetClientCachedBody like SetBody but it sends with an expiration datetime
// which is managed by the client-side (all major browsers supports this feature)
func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType string, modtime time.Time) {
if ctx.staticCachePassed(modtime) {
return
}
modtimeFormatted := modtime.UTC().Format(ctx.framework.Config.TimeFormat)
ctx.ResponseWriter.Header().Set(contentType, cType)
ctx.ResponseWriter.Header().Set(lastModified, modtimeFormatted)
ctx.SetStatusCode(status)
ctx.ResponseWriter.Write(bodyContent)
}
// 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
func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(1*time.Second)) {
ctx.ResponseWriter.Header().Del(contentType)
ctx.ResponseWriter.Header().Del(contentLength)
ctx.SetStatusCode(StatusNotModified)
return nil
}
ctx.ResponseWriter.Header().Set(contentType, fs.TypeByExtension(filename))
ctx.ResponseWriter.Header().Set(lastModified, modtime.UTC().Format(ctx.framework.Config.TimeFormat))
ctx.SetStatusCode(StatusOK)
var out io.Writer
if gzipCompression && ctx.clientAllowsGzip() {
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter)
defer fs.ReleaseGzipWriter(gzipWriter)
out = gzipWriter
} else {
out = ctx.ResponseWriter
}
_, err := io.Copy(out, content)
return errServeContent.With(err)
}
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
// 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
//
// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile
func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("%d", 404)
}
defer f.Close()
fi, _ := f.Stat()
if fi.IsDir() {
filename = path.Join(filename, "index.html")
f, err = os.Open(filename)
if err != nil {
return fmt.Errorf("%d", 404)
}
fi, _ = f.Stat()
}
return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression)
}
// SendFile sends file for force-download to the client
//
// Use this instead of ServeFile to 'force-download' bigger files to the client
func (ctx *Context) SendFile(filename string, destinationName string) {
ctx.ResponseWriter.Header().Set(contentDisposition, "attachment;filename="+destinationName)
ctx.ServeFile(filename, false)
}
// StreamWriter registers the given stream writer for populating
// response body.
//
// Access to context's and/or its' members is forbidden from writer.
//
// This function may be used in the following cases:
//
// * if response body is too big (more than iris.LimitRequestBodySize(if setted)).
// * if response body is streamed from slow external sources.
// * if response body must be streamed to the client in chunks.
// (aka `http server push`).
//
// receives a function which receives the response writer
// and returns false when it should stop writing, otherwise true in order to continue
func (ctx *Context) StreamWriter(writer func(w io.Writer) bool) {
w := ctx.ResponseWriter
notifyClosed := w.CloseNotify()
for {
select {
// response writer forced to close, exit.
case <-notifyClosed:
return
default:
shouldContinue := writer(w)
w.Flush()
if !shouldContinue {
return
}
}
}
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------------------Storage----------------------------------------------
// -----------------------User Values & Path parameters--------------------------------
// -------------------------------------------------------------------------------------
// ValuesLen returns the total length of the user values storage, some of them maybe path parameters
func (ctx *Context) ValuesLen() (n int) {
return len(ctx.values)
}
// Get returns the user's value from a key
// if doesn't exists returns nil
func (ctx *Context) Get(key string) interface{} {
return ctx.values.Get(key)
}
// GetFmt returns a value which has this format: func(format string, args ...interface{}) string
// if doesn't exists returns a function which returns an empty string.
//
// "translate" is the key of the i18n middlweare
// for more plaese look: https://github.com/iris-contrib/examples/tree/master/middleware_internationalization_i18n
func (ctx *Context) getFmt(key string) func(format string, args ...interface{}) string {
if v, ok := ctx.Get(key).(func(format string, args ...interface{}) string); ok {
return v
}
return func(format string, args ...interface{}) string { return "" }
}
// TranslateLanguageContextKey & TranslateFunctionContextKey are used by i18n handlers/middleware
// currently we have only one: https://github.com/iris-contrib/middleware/tree/master/i18n
// but you can use these keys to override the i18n's cookie name (TranslateLanguageContextKey)
// or to store new translate function by using the ctx.Set(iris.TranslateFunctionContextKey, theTrFunc)
var (
TranslateLanguageContextKey = "language"
TranslateFunctionContextKey = "translate"
)
// Translate is the i18n (localization) middleware's function, it just
// calls the ctx.getFmt("translate").
// "translate" is the key of the i18n middlweare
// for more plaese look: https://github.com/iris-contrib/examples/tree/master/middleware_internationalization_i18n
func (ctx *Context) Translate(format string, args ...interface{}) string {
return ctx.getFmt(TranslateFunctionContextKey)(format, args...)
}
// GetString same as Get but returns the value as string
// if nothing founds returns empty string ""
func (ctx *Context) GetString(key string) string {
if v, ok := ctx.Get(key).(string); ok {
return v
}
return ""
}
var errIntParse = errors.New("Unable to find or parse the integer, found: %#v")
// GetInt same as Get but tries to convert the return value as integer
// if nothing found or canno be parsed to integer it returns an error
func (ctx *Context) GetInt(key string) (int, error) {
v := ctx.Get(key)
if vint, ok := v.(int); ok {
return vint, nil
} else if vstring, sok := v.(string); sok {
return strconv.Atoi(vstring)
}
return -1, errIntParse.Format(v)
}
// Set sets a value to a key in the values map
func (ctx *Context) Set(key string, value interface{}) {
ctx.values.Set(key, value)
}
// VisitValues calls visitor for each existing context's temp values.
//
// visitor must not retain references to key and value after returning.
// Make key and/or value copies if you need storing them after returning.
func (ctx *Context) VisitValues(visitor func(string, interface{})) {
for i, n := 0, len(ctx.values); i < n; i++ {
kv := &ctx.values[i]
visitor(string(kv.key), kv.value)
}
}
// ParamsLen tries to return all the stored values which values are string, probably most of them will be the path parameters
func (ctx *Context) ParamsLen() (n int) {
ctx.VisitValues(func(kb string, vg interface{}) {
if _, ok := vg.(string); ok {
n++
}
})
return
}
// Param returns the string representation of the key's path named parameter's value
// same as GetString
func (ctx *Context) Param(key string) string {
return ctx.GetString(key)
}
// ParamDecoded returns a url-query-decoded path parameter's value
func (ctx *Context) ParamDecoded(key string) string {
return DecodeQuery(DecodeQuery(ctx.Param(key)))
}
// ParamInt returns the int representation of the key's path named parameter's value
// same as GetInt
func (ctx *Context) ParamInt(key string) (int, error) {
return ctx.GetInt(key)
}
// ParamInt64 returns the int64 representation of the key's path named parameter's value
func (ctx *Context) ParamInt64(key string) (int64, error) {
return strconv.ParseInt(ctx.Param(key), 10, 64)
}
// ParamsSentence returns a string implementation of all parameters that this context keeps
// hasthe form of key1=value1,key2=value2...
func (ctx *Context) ParamsSentence() string {
var buff bytes.Buffer
ctx.VisitValues(func(kb string, vg interface{}) {
v, ok := vg.(string)
if !ok {
return
}
k := string(kb)
buff.WriteString(k)
buff.WriteString("=")
buff.WriteString(v)
// we don't know where that (yet) stops so...
buff.WriteString(",")
})
result := buff.String()
if len(result) < 2 {
return ""
}
return result[0 : len(result)-1]
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------https://github.com/golang/net/blob/master/context/context.go--------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
return
}
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
func (ctx *Context) Done() <-chan struct{} {
return nil
}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
func (ctx *Context) Err() error {
return nil
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stores using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "golang.org/x/net/context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key = 0
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
func (ctx *Context) Value(key interface{}) interface{} {
if key == 0 {
return ctx.Request
}
if k, ok := key.(string); ok {
return ctx.GetString(k)
}
return nil
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------------------Session & Cookies------------------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// VisitAllCookies takes a visitor which loops on each (request's) cookie key and value
func (ctx *Context) VisitAllCookies(visitor func(key string, value string)) {
for _, cookie := range ctx.Request.Cookies() {
visitor(cookie.Name, cookie.Value)
}
}
// GetCookie returns cookie's value by it's name
// returns empty string if nothing was found
func (ctx *Context) GetCookie(name string) string {
cookie, err := ctx.Request.Cookie(name)
if err != nil {
return ""
}
return cookie.Value
}
// SetCookie adds a cookie
func (ctx *Context) SetCookie(cookie *http.Cookie) {
http.SetCookie(ctx.ResponseWriter, cookie)
}
// SetCookieKV adds a cookie, receives just a key(string) and a value(string)
//
// Expires on 2 hours by default(unchable)
// use ctx.SetCookie or http.SetCookie instead for more control.
func (ctx *Context) SetCookieKV(name, value string) {
c := &http.Cookie{}
c.Name = name
c.Value = value
c.HttpOnly = true
c.Expires = time.Now().Add(time.Duration(120) * time.Minute)
ctx.SetCookie(c)
}
// RemoveCookie deletes a cookie by it's name/key
func (ctx *Context) RemoveCookie(name string) {
c := &http.Cookie{}
c.Name = name
c.Value = ""
c.Path = "/"
c.HttpOnly = true
exp := time.Now().Add(-time.Duration(1) * time.Minute) //RFC says 1 second, but let's do it 1 minute to make sure is working...
c.Expires = exp
c.MaxAge = -1
ctx.SetCookie(c)
// delete request's cookie also, which is temporary available
ctx.Request.Header.Set("Cookie", "")
}
// Session returns the current session ( && flash messages )
func (ctx *Context) Session() sessions.Session {
if ctx.framework.sessions == nil { // this should never return nil but FOR ANY CASE, on future changes.
return nil
}
if ctx.session == nil {
ctx.session = ctx.framework.sessions.Start(ctx.ResponseWriter, ctx.Request)
}
return ctx.session
}
// SessionDestroy destroys the whole session, calls the provider's destroy and remove the cookie
func (ctx *Context) SessionDestroy() {
if sess := ctx.Session(); sess != nil {
ctx.framework.sessions.Destroy(ctx.ResponseWriter, ctx.Request)
}
}
var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`)
// MaxAge returns the "cache-control" request header's value
// seconds as int64
// if header not found or parse failed then it returns -1
func (ctx *Context) MaxAge() int64 {
header := ctx.RequestHeader(cacheControl)
if header == "" {
return -1
}
m := maxAgeExp.FindStringSubmatch(header)
if len(m) == 2 {
if v, err := strconv.Atoi(m[1]); err == nil {
return int64(v)
}
}
return -1
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ---------------------------Transactions & Response Writer Recording------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// Record transforms the context's basic and direct responseWriter to a ResponseRecorder
// which can be used to reset the body, reset headers, get the body,
// get & set the status code at any time and more
func (ctx *Context) Record() {
if w, ok := ctx.ResponseWriter.(*responseWriter); ok {
ctx.ResponseWriter = acquireResponseRecorder(w)
}
}
// Recorder returns the context's ResponseRecorder
// if not recording then it starts recording and returns the new context's ResponseRecorder
func (ctx *Context) Recorder() *ResponseRecorder {
ctx.Record()
return ctx.ResponseWriter.(*ResponseRecorder)
}
// IsRecording returns the response recorder and a true value
// when the response writer is recording the status code, body, headers and so on,
// else returns nil and false
func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
//NOTE:
// two return values in order to minimize the if statement:
// if (Recording) then writer = Recorder()
// instead we do: recorder,ok = Recording()
rr, ok := ctx.ResponseWriter.(*ResponseRecorder)
return rr, ok
}
// skipTransactionsContextKey set this to any value to stop executing next transactions
// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
const skipTransactionsContextKey = "__IRIS_TRANSACTIONS_SKIP___"
// SkipTransactions if called then skip the rest of the transactions
// or all of them if called before the first transaction
func (ctx *Context) SkipTransactions() {
ctx.Set(skipTransactionsContextKey, 1)
}
// TransactionsSkipped returns true if the transactions skipped or canceled at all.
func (ctx *Context) TransactionsSkipped() bool {
if n, err := ctx.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
return true
}
return false
}
// non-detailed error log for transacton unexpected panic
var errTransactionInterrupted = errors.New("Transaction Interrupted, recovery from panic:\n%s")
// BeginTransaction starts a scoped transaction.
//
// Can't say a lot here because it will take more than 200 lines to write about.
// You can search third-party articles or books on how Business Transaction works (it's quite simple, especially here).
//
// Note that this is unique and new
// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
// it's not covers all paths,
// such as databases, this should be managed by the libraries you use to make your database connection,
// this transaction scope is only for context's response.
// Transactions have their own middleware ecosystem also, look iris.go:UseTransaction.
//
// See https://github.com/iris-contrib/examples/tree/master/transactions for more
func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
// SILLY NOTE: use of manual pipe type in order of TransactionFunc
// in order to help editors complete the sentence here...
// do NOT begin a transaction when the previous transaction has been failed
// and it was requested scoped or SkipTransactions called manually.
if ctx.TransactionsSkipped() {
return
}
// start recording in order to be able to control the full response writer
ctx.Record()
// get a transaction scope from the pool by passing the temp context/
t := newTransaction(ctx)
defer func() {
if err := recover(); err != nil {
if ctx.framework.Config.IsDevelopment {
ctx.Log(errTransactionInterrupted.Format(err).Error())
}
// complete (again or not , doesn't matters) the scope without loud
t.Complete(nil)
// we continue as normal, no need to return here*
}
// write the temp contents to the original writer
t.Context.ResponseWriter.writeTo(ctx.ResponseWriter)
// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
// this is tricky but nessecery if we want ctx.EmitError to work inside transactions
t.Context.ResponseWriter = ctx.ResponseWriter
}()
// run the worker with its context clone inside.
pipe(t)
}
// Log logs to the iris defined logger
func (ctx *Context) Log(format string, a ...interface{}) {
ctx.framework.Logger.Printf(format, a...)
}
// Framework returns the Iris instance, containing the configuration and all other fields
func (ctx *Context) Framework() *Framework {
return ctx.framework
}