├── testdata ├── i18n │ ├── messages │ │ ├── invalid_message_file_name.txt │ │ ├── english_messages2.en │ │ ├── dutch_messages.nl │ │ └── english_messages.en │ └── config │ │ └── test_app.conf ├── public │ └── js │ │ └── sessvars.js ├── app │ └── views │ │ ├── footer.html │ │ ├── hotels │ │ └── show.html │ │ └── header.html └── conf │ ├── routes │ └── app.conf ├── AUTHORS ├── skeleton ├── .gitignore ├── public │ ├── img │ │ └── favicon.png │ └── fonts │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 ├── app │ ├── views │ │ ├── footer.html │ │ ├── errors │ │ │ ├── 500.html │ │ │ └── 404.html │ │ ├── flash.html │ │ ├── App │ │ │ └── Index.html │ │ ├── header.html │ │ └── debug.html │ ├── controllers │ │ └── app.go │ └── init.go ├── tests │ └── apptest.go ├── messages │ └── sample.en ├── conf │ ├── routes │ └── app.conf.template └── README.md ├── templates └── errors │ ├── 404.xml │ ├── 403.txt │ ├── 403.xml │ ├── 404.txt │ ├── 405.txt │ ├── 405.xml │ ├── 403.json │ ├── 404.json │ ├── 405.json │ ├── 500.json │ ├── 500.xml │ ├── 403.html │ ├── 405.html │ ├── 500.html │ ├── 500.txt │ ├── 404.html │ ├── 404-dev.html │ └── 500-dev.html ├── version.yaml ├── .gitignore ├── version.go ├── libs.go ├── panic.go ├── cache ├── inmemory_test.go ├── memcached_test.go ├── redis_test.go ├── init.go ├── inmemory.go ├── serialization_test.go ├── serialization.go ├── memcached.go ├── cache.go ├── cache_test.go └── redis.go ├── LICENSE ├── filter.go ├── results_test.go ├── invoker.go ├── compress_test.go ├── field.go ├── session_test.go ├── flash.go ├── validation_test.go ├── .travis.yml ├── fakeapp_test.go ├── intercept_test.go ├── util_test.go ├── README.md ├── server_test.go ├── invoker_test.go ├── filterconfig_test.go ├── params.go ├── errors.go ├── validators.go ├── http.go ├── params_test.go ├── session.go ├── watcher.go ├── compress.go ├── intercept.go ├── server.go ├── CONTRIBUTING.md ├── filterconfig.go ├── i18n.go ├── validation.go ├── util.go └── testing └── testsuite_test.go /testdata/i18n/messages/invalid_message_file_name.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # TODO Revel Framework Authors Information 2 | -------------------------------------------------------------------------------- /skeleton/.gitignore: -------------------------------------------------------------------------------- 1 | test-results/ 2 | tmp/ 3 | routes/ 4 | -------------------------------------------------------------------------------- /testdata/i18n/messages/english_messages2.en: -------------------------------------------------------------------------------- 1 | greeting2=Yo! 2 | -------------------------------------------------------------------------------- /testdata/public/js/sessvars.js: -------------------------------------------------------------------------------- 1 | console.log('Test file'); 2 | -------------------------------------------------------------------------------- /templates/errors/404.xml: -------------------------------------------------------------------------------- 1 | {{.Error.Description}} 2 | -------------------------------------------------------------------------------- /templates/errors/403.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | 3 | {{.Error.Description}} 4 | -------------------------------------------------------------------------------- /templates/errors/403.xml: -------------------------------------------------------------------------------- 1 | {{.Error.Description}} 2 | -------------------------------------------------------------------------------- /templates/errors/404.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | 3 | {{.Error.Description}} 4 | -------------------------------------------------------------------------------- /templates/errors/405.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | 3 | {{.Error.Description}} 4 | -------------------------------------------------------------------------------- /version.yaml: -------------------------------------------------------------------------------- 1 | version: 0.13.0-dev 2 | buildDate: TBD 3 | minimumGo: >= go1.4 4 | -------------------------------------------------------------------------------- /templates/errors/405.xml: -------------------------------------------------------------------------------- 1 | {{.Error.Description}} 2 | -------------------------------------------------------------------------------- /skeleton/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoping378/revel/master/skeleton/public/img/favicon.png -------------------------------------------------------------------------------- /templates/errors/403.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/405.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /templates/errors/500.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{{js .Error.Title}}", 3 | "description": "{{js .Error.Description}}" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | routes/ 3 | test-results/ 4 | revel/revel 5 | 6 | # editor 7 | *.swp 8 | 9 | .idea/ 10 | *.iml 11 | 12 | -------------------------------------------------------------------------------- /skeleton/app/views/footer.html: -------------------------------------------------------------------------------- 1 | {{if eq .RunMode "dev"}} 2 | {{template "debug.html" .}} 3 | {{end}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/errors/500.xml: -------------------------------------------------------------------------------- 1 | 2 | {{.Error.Title}} 3 | {{.Error.Description}} 4 | 5 | -------------------------------------------------------------------------------- /skeleton/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoping378/revel/master/skeleton/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /skeleton/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoping378/revel/master/skeleton/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /skeleton/public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoping378/revel/master/skeleton/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /testdata/i18n/messages/dutch_messages.nl: -------------------------------------------------------------------------------- 1 | greeting=Hallo 2 | greeting.name=Rob 3 | greeting.suffix=, welkom bij Revel! 4 | 5 | [NL] 6 | greeting=Goeiedag 7 | 8 | [BE] 9 | greeting=Hallokes 10 | -------------------------------------------------------------------------------- /skeleton/app/controllers/app.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/revel/revel" 4 | 5 | type App struct { 6 | *revel.Controller 7 | } 8 | 9 | func (c App) Index() revel.Result { 10 | return c.Render() 11 | } 12 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | const ( 4 | // Current Revel version 5 | Version = "0.13.1" 6 | 7 | // Latest commit date 8 | BuildDate = "2016-06-06" 9 | 10 | // Minimum required Go version 11 | MinimumGoVersion = ">= go1.4" 12 | ) 13 | -------------------------------------------------------------------------------- /templates/errors/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forbidden 5 | 6 | 7 | {{with .Error}} 8 |

9 | {{.Title}} 10 |

11 |

12 | {{.Description}} 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/errors/405.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Method not allowed 5 | 6 | 7 | {{with .Error}} 8 |

9 | {{.Title}} 10 |

11 |

12 | {{.Description}} 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application error 5 | 6 | 7 | {{if .DevMode}} 8 | {{template "errors/500-dev.html" .}} 9 | {{else}} 10 |

Oops, an error occured

11 |

12 | This exception has been logged. 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/errors/500.txt: -------------------------------------------------------------------------------- 1 | {{.Error.Title}} 2 | {{.Error.Description}} 3 | 4 | {{if eq .RunMode "dev"}} 5 | {{with .Error}} 6 | {{if .Path}} 7 | ---------- 8 | In {{.Path}} {{if .Line}}(around line {{.Line}}){{end}} 9 | 10 | {{range .ContextSource}} 11 | {{if .IsError}}>{{else}} {{end}} {{.Line}}: {{.Source}}{{end}} 12 | 13 | {{end}} 14 | {{end}} 15 | {{end}} 16 | -------------------------------------------------------------------------------- /skeleton/app/views/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application error 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/500-dev.html" .}} 9 | {{else}} 10 |

Oops, an error occured

11 |

12 | This exception has been logged. 13 |

14 | {{end}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /skeleton/app/views/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not found 5 | 6 | 7 | {{if eq .RunMode "dev"}} 8 | {{template "errors/404-dev.html" .}} 9 | {{else}} 10 | {{with .Error}} 11 |

12 | {{.Title}} 13 |

14 |

15 | {{.Description}} 16 |

17 | {{end}} 18 | {{end}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not found 5 | 6 | 7 | 8 | {{if .DevMode}} 9 | 10 | {{template "errors/404-dev.html" .}} 11 | 12 | {{else}} 13 | 14 | {{with .Error}} 15 |

16 | {{.Title}} 17 |

18 |

19 | {{.Description}} 20 |

21 | {{end}} 22 | 23 | {{end}} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /skeleton/app/views/flash.html: -------------------------------------------------------------------------------- 1 | {{if .flash.success}} 2 |
3 | {{.flash.success}} 4 |
5 | {{end}} 6 | 7 | {{if or .errors .flash.error}} 8 |
9 | {{if .flash.error}} 10 | {{.flash.error}} 11 | {{end}} 12 | 17 |
18 | {{end}} 19 | -------------------------------------------------------------------------------- /skeleton/tests/apptest.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/revel/revel/testing" 4 | 5 | type AppTest struct { 6 | testing.TestSuite 7 | } 8 | 9 | func (t *AppTest) Before() { 10 | println("Set up") 11 | } 12 | 13 | func (t *AppTest) TestThatIndexPageWorks() { 14 | t.Get("/") 15 | t.AssertOk() 16 | t.AssertContentType("text/html; charset=utf-8") 17 | } 18 | 19 | func (t *AppTest) After() { 20 | println("Tear down") 21 | } 22 | -------------------------------------------------------------------------------- /testdata/app/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /skeleton/messages/sample.en: -------------------------------------------------------------------------------- 1 | # Sample messages file for the English language (en) 2 | # Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 3 | # Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 4 | # See also: 5 | # - http://www.rfc-editor.org/rfc/bcp/bcp47.txt 6 | # - http://www.w3.org/International/questions/qa-accept-lang-locales 7 | 8 | -------------------------------------------------------------------------------- /skeleton/app/views/App/Index.html: -------------------------------------------------------------------------------- 1 | {{set . "title" "Home"}} 2 | {{template "header.html" .}} 3 | 4 |
5 |
6 |
7 |

It works!

8 |

9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 | {{template "flash.html" .}} 17 |
18 |
19 |
20 | 21 | {{template "footer.html" .}} 22 | -------------------------------------------------------------------------------- /testdata/i18n/messages/english_messages.en: -------------------------------------------------------------------------------- 1 | greeting=Hello 2 | greeting.name=Rob 3 | greeting.suffix=, welcome to Revel! 4 | 5 | folded=Greeting is '%(greeting)s' 6 | folded.arguments=%(greeting.name)s is %d years old 7 | 8 | arguments.string=My name is %s 9 | arguments.hex=The number %d in hexadecimal notation would be %x 10 | arguments.none=No arguments here son 11 | 12 | only_exists_in_default=Default 13 | 14 | [AU] 15 | greeting=G'day 16 | 17 | [US] 18 | greeting=Howdy 19 | 20 | [GB] 21 | greeting=All right -------------------------------------------------------------------------------- /skeleton/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | GET / App.Index 8 | 9 | # Ignore favicon requests 10 | GET /favicon.ico 404 11 | 12 | # Map static resources from the /app/public folder to the /public path 13 | GET /public/*filepath Static.Serve("public") 14 | 15 | # Catch all 16 | * /:controller/:action :controller.:action 17 | -------------------------------------------------------------------------------- /testdata/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | module:testrunner 6 | 7 | GET /hotels Hotels.Index 8 | GET /hotels/:id Hotels.Show 9 | GET /hotels/:id/booking Hotels.Book 10 | 11 | # Map static resources from the /app/public folder to the /public path 12 | GET /public/*filepath Static.Serve("public") 13 | GET /favicon.ico Static.Serve("public/img","favicon.png") 14 | 15 | # Catch all 16 | * /:controller/:action :controller.:action 17 | -------------------------------------------------------------------------------- /libs.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "io" 8 | ) 9 | 10 | // Sign a given string with the app-configured secret key. 11 | // If no secret key is set, returns the empty string. 12 | // Return the signature in base64 (URLEncoding). 13 | func Sign(message string) string { 14 | if len(secretKey) == 0 { 15 | return "" 16 | } 17 | mac := hmac.New(sha1.New, secretKey) 18 | io.WriteString(mac, message) 19 | return hex.EncodeToString(mac.Sum(nil)) 20 | } 21 | 22 | // Verify returns true if the given signature is correct for the given message. 23 | // e.g. it matches what we generate with Sign() 24 | func Verify(message, sig string) bool { 25 | return hmac.Equal([]byte(sig), []byte(Sign(message))) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/i18n/config/test_app.conf: -------------------------------------------------------------------------------- 1 | app.name={{ .AppName }} 2 | app.secret={{ .Secret }} 3 | http.addr= 4 | http.port=9000 5 | cookie.prefix=REVEL 6 | 7 | i18n.default_language=en 8 | i18n.cookie=APP_LANG 9 | 10 | [dev] 11 | results.pretty=true 12 | results.staging=true 13 | watch=true 14 | 15 | module.testrunner = github.com/revel/modules/testrunner 16 | module.static=github.com/revel/modules/static 17 | 18 | log.trace.output = off 19 | log.info.output = stderr 20 | log.warn.output = stderr 21 | log.error.output = stderr 22 | 23 | [prod] 24 | results.pretty=false 25 | results.staging=false 26 | watch=false 27 | 28 | module.testrunner = 29 | 30 | log.trace.output = off 31 | log.info.output = off 32 | log.warn.output = %(app.name)s.log 33 | log.error.output = %(app.name)s.log 34 | -------------------------------------------------------------------------------- /skeleton/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{range .moreStyles}} 13 | 14 | {{end}} 15 | {{range .moreScripts}} 16 | 17 | {{end}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /testdata/app/views/hotels/show.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 3 |

View hotel

4 | 5 | {{with .hotel}} 6 |
7 | 8 |

9 | Name: {{.Name}} 10 |

11 |

12 | Address: {{.Address}} 13 |

14 |

15 | City: {{.City}} 16 |

17 |

18 | State: {{.State}} 19 |

20 |

21 | Zip: {{.Zip}} 22 |

23 |

24 | Country: {{.Country}} 25 |

26 |

27 | Nightly rate: {{.Price}} 28 |

29 | 30 |

31 | 32 | Back to search 33 |

34 |
35 | {{end}} 36 | 37 | {{template "footer.html" .}} 38 | -------------------------------------------------------------------------------- /testdata/conf/app.conf: -------------------------------------------------------------------------------- 1 | # Application 2 | app.name=Booking example 3 | app.secret=secret 4 | 5 | # Server 6 | http.addr= 7 | http.port=9000 8 | http.ssl=false 9 | http.sslcert= 10 | http.sslkey= 11 | 12 | # Logging 13 | log.trace.output = stderr 14 | log.info.output = stderr 15 | log.warn.output = stderr 16 | log.error.output = stderr 17 | 18 | log.trace.prefix = "TRACE " 19 | log.info.prefix = "INFO " 20 | log.warn.prefix = "WARN " 21 | log.error.prefix = "ERROR " 22 | 23 | db.import = github.com/mattn/go-sqlite3 24 | db.driver = sqlite3 25 | db.spec = :memory: 26 | 27 | build.tags=gorp 28 | 29 | module.jobs=github.com/revel/modules/jobs 30 | module.static=github.com/revel/modules/static 31 | 32 | [dev] 33 | mode.dev=true 34 | watch=true 35 | module.testrunner=github.com/revel/modules/testrunner 36 | 37 | [prod] 38 | watch=false 39 | module.testrunner= 40 | 41 | log.trace.output = off 42 | log.info.output = off 43 | log.warn.output = stderr 44 | log.error.output = stderr 45 | -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | // PanicFilter wraps the action invocation in a protective defer blanket that 8 | // converts panics into 500 error pages. 9 | func PanicFilter(c *Controller, fc []Filter) { 10 | defer func() { 11 | if err := recover(); err != nil { 12 | handleInvocationPanic(c, err) 13 | } 14 | }() 15 | fc[0](c, fc[1:]) 16 | } 17 | 18 | // This function handles a panic in an action invocation. 19 | // It cleans up the stack trace, logs it, and displays an error page. 20 | func handleInvocationPanic(c *Controller, err interface{}) { 21 | error := NewErrorFromPanic(err) 22 | if error == nil && DevMode { 23 | // Only show the sensitive information in the debug stack trace in development mode, not production 24 | ERROR.Print(err, "\n", string(debug.Stack())) 25 | c.Response.Out.WriteHeader(500) 26 | c.Response.Out.Write(debug.Stack()) 27 | return 28 | } 29 | 30 | ERROR.Print(err, "\n", error.Stack) 31 | c.Result = c.RenderError(error) 32 | } 33 | -------------------------------------------------------------------------------- /cache/inmemory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache { 9 | return NewInMemoryCache(defaultExpiration) 10 | } 11 | 12 | // Test typical cache interactions 13 | func TestInMemoryCache_TypicalGetSet(t *testing.T) { 14 | typicalGetSet(t, newInMemoryCache) 15 | } 16 | 17 | // Test the increment-decrement cases 18 | func TestInMemoryCache_IncrDecr(t *testing.T) { 19 | incrDecr(t, newInMemoryCache) 20 | } 21 | 22 | func TestInMemoryCache_Expiration(t *testing.T) { 23 | expiration(t, newInMemoryCache) 24 | } 25 | 26 | func TestInMemoryCache_EmptyCache(t *testing.T) { 27 | emptyCache(t, newInMemoryCache) 28 | } 29 | 30 | func TestInMemoryCache_Replace(t *testing.T) { 31 | testReplace(t, newInMemoryCache) 32 | } 33 | 34 | func TestInMemoryCache_Add(t *testing.T) { 35 | testAdd(t, newInMemoryCache) 36 | } 37 | 38 | func TestInMemoryCache_GetMulti(t *testing.T) { 39 | testGetMulti(t, newInMemoryCache) 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2012-2016 The Revel Framework Authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | type Filter func(c *Controller, filterChain []Filter) 4 | 5 | // Filters is the default set of global filters. 6 | // It may be set by the application on initialization. 7 | var Filters = []Filter{ 8 | PanicFilter, // Recover from panics and display an error page instead. 9 | RouterFilter, // Use the routing table to select the right Action. 10 | FilterConfiguringFilter, // A hook for adding or removing per-Action filters. 11 | ParamsFilter, // Parse parameters into Controller.Params. 12 | SessionFilter, // Restore and write the session cookie. 13 | FlashFilter, // Restore and write the flash cookie. 14 | ValidationFilter, // Restore kept validation errors and save new ones from cookie. 15 | I18nFilter, // Resolve the requested language. 16 | InterceptorFilter, // Run interceptors around the action. 17 | CompressFilter, // Compress the result. 18 | ActionInvoker, // Invoke the action. 19 | } 20 | 21 | // NilFilter and NilChain are helpful in writing filter tests. 22 | var ( 23 | NilFilter = func(_ *Controller, _ []Filter) {} 24 | NilChain = []Filter{NilFilter} 25 | ) 26 | -------------------------------------------------------------------------------- /templates/errors/404-dev.html: -------------------------------------------------------------------------------- 1 | 45 | 46 | 56 |
57 |

These routes have been tried, in this order :

58 |
    59 | {{range .Router.Routes}} 60 |
  1. {{pad .Method 10}}{{pad .Path 50}}{{.Action}}
  2. 61 | {{end}} 62 |
63 |
64 | -------------------------------------------------------------------------------- /cache/memcached_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // These tests require memcached running on localhost:11211 (the default) 10 | const testServer = "localhost:11211" 11 | 12 | var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 13 | c, err := net.Dial("tcp", testServer) 14 | if err == nil { 15 | c.Write([]byte("flush_all\r\n")) 16 | c.Close() 17 | return NewMemcachedCache([]string{testServer}, defaultExpiration) 18 | } 19 | t.Errorf("couldn't connect to memcached on %s", testServer) 20 | t.FailNow() 21 | panic("") 22 | } 23 | 24 | func TestMemcachedCache_TypicalGetSet(t *testing.T) { 25 | typicalGetSet(t, newMemcachedCache) 26 | } 27 | 28 | func TestMemcachedCache_IncrDecr(t *testing.T) { 29 | incrDecr(t, newMemcachedCache) 30 | } 31 | 32 | func TestMemcachedCache_Expiration(t *testing.T) { 33 | expiration(t, newMemcachedCache) 34 | } 35 | 36 | func TestMemcachedCache_EmptyCache(t *testing.T) { 37 | emptyCache(t, newMemcachedCache) 38 | } 39 | 40 | func TestMemcachedCache_Replace(t *testing.T) { 41 | testReplace(t, newMemcachedCache) 42 | } 43 | 44 | func TestMemcachedCache_Add(t *testing.T) { 45 | testAdd(t, newMemcachedCache) 46 | } 47 | 48 | func TestMemcachedCache_GetMulti(t *testing.T) { 49 | testGetMulti(t, newMemcachedCache) 50 | } 51 | -------------------------------------------------------------------------------- /cache/redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/revel/config" 9 | "github.com/revel/revel" 10 | ) 11 | 12 | // These tests require redis server running on localhost:6379 (the default) 13 | const redisTestServer = "localhost:6379" 14 | 15 | var newRedisCache = func(t *testing.T, defaultExpiration time.Duration) Cache { 16 | revel.Config = config.NewContext() 17 | 18 | c, err := net.Dial("tcp", redisTestServer) 19 | if err == nil { 20 | c.Write([]byte("flush_all\r\n")) 21 | c.Close() 22 | redisCache := NewRedisCache(redisTestServer, "", defaultExpiration) 23 | redisCache.Flush() 24 | return redisCache 25 | } 26 | t.Errorf("couldn't connect to redis on %s", redisTestServer) 27 | t.FailNow() 28 | panic("") 29 | } 30 | 31 | func TestRedisCache_TypicalGetSet(t *testing.T) { 32 | typicalGetSet(t, newRedisCache) 33 | } 34 | 35 | func TestRedisCache_IncrDecr(t *testing.T) { 36 | incrDecr(t, newRedisCache) 37 | } 38 | 39 | func TestRedisCache_Expiration(t *testing.T) { 40 | expiration(t, newRedisCache) 41 | } 42 | 43 | func TestRedisCache_EmptyCache(t *testing.T) { 44 | emptyCache(t, newRedisCache) 45 | } 46 | 47 | func TestRedisCache_Replace(t *testing.T) { 48 | testReplace(t, newRedisCache) 49 | } 50 | 51 | func TestRedisCache_Add(t *testing.T) { 52 | testAdd(t, newRedisCache) 53 | } 54 | 55 | func TestRedisCache_GetMulti(t *testing.T) { 56 | testGetMulti(t, newRedisCache) 57 | } 58 | -------------------------------------------------------------------------------- /results_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Test that the render response is as expected. 10 | func TestBenchmarkRender(t *testing.T) { 11 | startFakeBookingApp() 12 | resp := httptest.NewRecorder() 13 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 14 | c.SetAction("Hotels", "Show") 15 | result := Hotels{c}.Show(3) 16 | result.Apply(c.Request, c.Response) 17 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 18 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 19 | } 20 | } 21 | 22 | func BenchmarkRenderChunked(b *testing.B) { 23 | startFakeBookingApp() 24 | resp := httptest.NewRecorder() 25 | resp.Body = nil 26 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 27 | c.SetAction("Hotels", "Show") 28 | Config.SetOption("results.chunked", "true") 29 | b.ResetTimer() 30 | 31 | hotels := Hotels{c} 32 | for i := 0; i < b.N; i++ { 33 | hotels.Show(3).Apply(c.Request, c.Response) 34 | } 35 | } 36 | 37 | func BenchmarkRenderNotChunked(b *testing.B) { 38 | startFakeBookingApp() 39 | resp := httptest.NewRecorder() 40 | resp.Body = nil 41 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 42 | c.SetAction("Hotels", "Show") 43 | Config.SetOption("results.chunked", "false") 44 | b.ResetTimer() 45 | 46 | hotels := Hotels{c} 47 | for i := 0; i < b.N; i++ { 48 | hotels.Show(3).Apply(c.Request, c.Response) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /invoker.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | 7 | "golang.org/x/net/websocket" 8 | ) 9 | 10 | var ( 11 | controllerType = reflect.TypeOf(Controller{}) 12 | controllerPtrType = reflect.TypeOf(&Controller{}) 13 | websocketType = reflect.TypeOf((*websocket.Conn)(nil)) 14 | ) 15 | 16 | func ActionInvoker(c *Controller, _ []Filter) { 17 | // Instantiate the method. 18 | methodValue := reflect.ValueOf(c.AppController).MethodByName(c.MethodType.Name) 19 | 20 | // Collect the values for the method's arguments. 21 | var methodArgs []reflect.Value 22 | for _, arg := range c.MethodType.Args { 23 | // If they accept a websocket connection, treat that arg specially. 24 | var boundArg reflect.Value 25 | if arg.Type == websocketType { 26 | boundArg = reflect.ValueOf(c.Request.Websocket) 27 | } else { 28 | boundArg = Bind(c.Params, arg.Name, arg.Type) 29 | // #756 - If the argument is a closer, defer a Close call, 30 | // so we don't risk on leaks. 31 | if closer, ok := boundArg.Interface().(io.Closer); ok { 32 | defer closer.Close() 33 | } 34 | } 35 | methodArgs = append(methodArgs, boundArg) 36 | } 37 | 38 | var resultValue reflect.Value 39 | if methodValue.Type().IsVariadic() { 40 | resultValue = methodValue.CallSlice(methodArgs)[0] 41 | } else { 42 | resultValue = methodValue.Call(methodArgs)[0] 43 | } 44 | if resultValue.Kind() == reflect.Interface && !resultValue.IsNil() { 45 | c.Result = resultValue.Interface().(Result) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/app/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | {{range .moreStyles}} 9 | 10 | {{end}} 11 | 12 | 13 | {{range .moreScripts}} 14 | 15 | {{end}} 16 | 17 | 18 | 19 | 33 | 34 |
35 | {{if .flash.error}} 36 |

37 | {{.flash.error}} 38 |

39 | {{end}} 40 | {{if .flash.success}} 41 |

42 | {{.flash.success}} 43 |

44 | {{end}} 45 | 46 | -------------------------------------------------------------------------------- /compress_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Test that the render response is as expected. 10 | func TestBenchmarkCompressed(t *testing.T) { 11 | startFakeBookingApp() 12 | resp := httptest.NewRecorder() 13 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 14 | c.SetAction("Hotels", "Show") 15 | Config.SetOption("results.compressed", "true") 16 | result := Hotels{c}.Show(3) 17 | result.Apply(c.Request, c.Response) 18 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 19 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 20 | } 21 | } 22 | 23 | func BenchmarkRenderCompressed(b *testing.B) { 24 | startFakeBookingApp() 25 | resp := httptest.NewRecorder() 26 | resp.Body = nil 27 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 28 | c.SetAction("Hotels", "Show") 29 | Config.SetOption("results.compressed", "true") 30 | b.ResetTimer() 31 | 32 | hotels := Hotels{c} 33 | for i := 0; i < b.N; i++ { 34 | hotels.Show(3).Apply(c.Request, c.Response) 35 | } 36 | } 37 | 38 | func BenchmarkRenderUnCompressed(b *testing.B) { 39 | startFakeBookingApp() 40 | resp := httptest.NewRecorder() 41 | resp.Body = nil 42 | c := NewController(NewRequest(showRequest), NewResponse(resp)) 43 | c.SetAction("Hotels", "Show") 44 | Config.SetOption("results.compressed", "false") 45 | b.ResetTimer() 46 | 47 | hotels := Hotels{c} 48 | for i := 0; i < b.N; i++ { 49 | hotels.Show(3).Apply(c.Request, c.Response) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /skeleton/app/views/debug.html: -------------------------------------------------------------------------------- 1 | 20 | 44 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /skeleton/app/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/revel/revel" 4 | 5 | func init() { 6 | // Filters is the default set of global filters. 7 | revel.Filters = []revel.Filter{ 8 | revel.PanicFilter, // Recover from panics and display an error page instead. 9 | revel.RouterFilter, // Use the routing table to select the right Action 10 | revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters. 11 | revel.ParamsFilter, // Parse parameters into Controller.Params. 12 | revel.SessionFilter, // Restore and write the session cookie. 13 | revel.FlashFilter, // Restore and write the flash cookie. 14 | revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie. 15 | revel.I18nFilter, // Resolve the requested language 16 | HeaderFilter, // Add some security based headers 17 | revel.InterceptorFilter, // Run interceptors around the action. 18 | revel.CompressFilter, // Compress the result. 19 | revel.ActionInvoker, // Invoke the action. 20 | } 21 | 22 | // register startup functions with OnAppStart 23 | // ( order dependent ) 24 | // revel.OnAppStart(InitDB) 25 | // revel.OnAppStart(FillCache) 26 | } 27 | 28 | // TODO turn this into revel.HeaderFilter 29 | // should probably also have a filter for CSRF 30 | // not sure if it can go in the same filter or not 31 | var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) { 32 | // Add some common security headers 33 | c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN") 34 | c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block") 35 | c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff") 36 | 37 | fc[0](c, fc[1:]) // Execute the next filter stage. 38 | } 39 | -------------------------------------------------------------------------------- /cache/init.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/revel/revel" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | revel.OnAppStart(func() { 11 | // Set the default expiration time. 12 | defaultExpiration := time.Hour // The default for the default is one hour. 13 | if expireStr, found := revel.Config.String("cache.expires"); found { 14 | var err error 15 | if defaultExpiration, err = time.ParseDuration(expireStr); err != nil { 16 | panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error()) 17 | } 18 | } 19 | 20 | // make sure you aren't trying to use both memcached and redis 21 | if revel.Config.BoolDefault("cache.memcached", false) && revel.Config.BoolDefault("cache.redis", false) { 22 | panic("You've configured both memcached and redis, please only include configuration for one cache!") 23 | } 24 | 25 | // Use memcached? 26 | if revel.Config.BoolDefault("cache.memcached", false) { 27 | hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",") 28 | if len(hosts) == 0 { 29 | panic("Memcache enabled but no memcached hosts specified!") 30 | } 31 | 32 | Instance = NewMemcachedCache(hosts, defaultExpiration) 33 | return 34 | } 35 | 36 | // Use Redis (share same config as memcached)? 37 | if revel.Config.BoolDefault("cache.redis", false) { 38 | hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",") 39 | if len(hosts) == 0 { 40 | panic("Redis enabled but no Redis hosts specified!") 41 | } 42 | if len(hosts) > 1 { 43 | panic("Redis currently only supports one host!") 44 | } 45 | password := revel.Config.StringDefault("cache.redis.password", "") 46 | Instance = NewRedisCache(hosts[0], password, defaultExpiration) 47 | return 48 | } 49 | 50 | // By default, use the in-memory cache. 51 | Instance = NewInMemoryCache(defaultExpiration) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // Field represents a data field that may be collected in a web form. 9 | type Field struct { 10 | Name string 11 | Error *ValidationError 12 | renderArgs map[string]interface{} 13 | } 14 | 15 | func NewField(name string, renderArgs map[string]interface{}) *Field { 16 | err, _ := renderArgs["errors"].(map[string]*ValidationError)[name] 17 | return &Field{ 18 | Name: name, 19 | Error: err, 20 | renderArgs: renderArgs, 21 | } 22 | } 23 | 24 | // Id returns an identifier suitable for use as an HTML id. 25 | func (f *Field) Id() string { 26 | return strings.Replace(f.Name, ".", "_", -1) 27 | } 28 | 29 | // Flash returns the flashed value of this Field. 30 | func (f *Field) Flash() string { 31 | v, _ := f.renderArgs["flash"].(map[string]string)[f.Name] 32 | return v 33 | } 34 | 35 | // FlashArray returns the flashed value of this Field as a list split on comma. 36 | func (f *Field) FlashArray() []string { 37 | v := f.Flash() 38 | if v == "" { 39 | return []string{} 40 | } 41 | return strings.Split(v, ",") 42 | } 43 | 44 | // Value returns the current value of this Field. 45 | func (f *Field) Value() interface{} { 46 | pieces := strings.Split(f.Name, ".") 47 | answer, ok := f.renderArgs[pieces[0]] 48 | if !ok { 49 | return "" 50 | } 51 | 52 | val := reflect.ValueOf(answer) 53 | for i := 1; i < len(pieces); i++ { 54 | if val.Kind() == reflect.Ptr { 55 | val = val.Elem() 56 | } 57 | val = val.FieldByName(pieces[i]) 58 | if !val.IsValid() { 59 | return "" 60 | } 61 | } 62 | 63 | return val.Interface() 64 | } 65 | 66 | // ErrorClass returns ERROR_CLASS if this field has a validation error, else empty string. 67 | func (f *Field) ErrorClass() string { 68 | if f.Error != nil { 69 | if errorClass, ok := f.renderArgs["ERROR_CLASS"]; ok { 70 | return errorClass.(string) 71 | } else { 72 | return ERROR_CLASS 73 | } 74 | } 75 | return "" 76 | } 77 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSessionRestore(t *testing.T) { 10 | expireAfterDuration = 0 11 | originSession := make(Session) 12 | originSession["foo"] = "foo" 13 | originSession["bar"] = "bar" 14 | cookie := originSession.Cookie() 15 | if !cookie.Expires.IsZero() { 16 | t.Error("incorrect cookie expire", cookie.Expires) 17 | } 18 | 19 | restoredSession := GetSessionFromCookie(cookie) 20 | for k, v := range originSession { 21 | if restoredSession[k] != v { 22 | t.Errorf("session restore failed session[%s] != %s", k, v) 23 | } 24 | } 25 | } 26 | 27 | func TestSessionExpire(t *testing.T) { 28 | expireAfterDuration = time.Hour 29 | session := make(Session) 30 | session["user"] = "Tom" 31 | var cookie *http.Cookie 32 | for i := 0; i < 3; i++ { 33 | cookie = session.Cookie() 34 | time.Sleep(time.Second) 35 | session = GetSessionFromCookie(cookie) 36 | } 37 | expectExpire := time.Now().Add(expireAfterDuration) 38 | if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() { 39 | t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second)) 40 | } 41 | if cookie.Expires.Unix() > expectExpire.Unix() { 42 | t.Error("expect expires", cookie.Expires, "before", expectExpire) 43 | } 44 | 45 | session.SetNoExpiration() 46 | for i := 0; i < 3; i++ { 47 | cookie = session.Cookie() 48 | session = GetSessionFromCookie(cookie) 49 | } 50 | cookie = session.Cookie() 51 | if !cookie.Expires.IsZero() { 52 | t.Error("expect cookie expires is zero") 53 | } 54 | 55 | session.SetDefaultExpiration() 56 | cookie = session.Cookie() 57 | expectExpire = time.Now().Add(expireAfterDuration) 58 | if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() { 59 | t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second)) 60 | } 61 | if cookie.Expires.Unix() > expectExpire.Unix() { 62 | t.Error("expect expires", cookie.Expires, "before", expectExpire) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cache/inmemory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "github.com/revel/revel" 6 | "github.com/robfig/go-cache" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | type InMemoryCache struct { 12 | cache.Cache 13 | } 14 | 15 | func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache { 16 | return InMemoryCache{*cache.New(defaultExpiration, time.Minute)} 17 | } 18 | 19 | func (c InMemoryCache) Get(key string, ptrValue interface{}) error { 20 | value, found := c.Cache.Get(key) 21 | if !found { 22 | return ErrCacheMiss 23 | } 24 | 25 | v := reflect.ValueOf(ptrValue) 26 | if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() { 27 | v.Elem().Set(reflect.ValueOf(value)) 28 | return nil 29 | } 30 | 31 | err := fmt.Errorf("revel/cache: attempt to get %s, but can not set value %v", key, v) 32 | revel.ERROR.Println(err) 33 | return err 34 | } 35 | 36 | func (c InMemoryCache) GetMulti(keys ...string) (Getter, error) { 37 | return c, nil 38 | } 39 | 40 | func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error { 41 | // NOTE: go-cache understands the values of DEFAULT and FOREVER 42 | c.Cache.Set(key, value, expires) 43 | return nil 44 | } 45 | 46 | func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error { 47 | err := c.Cache.Add(key, value, expires) 48 | if err == cache.ErrKeyExists { 49 | return ErrNotStored 50 | } 51 | return err 52 | } 53 | 54 | func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error { 55 | if err := c.Cache.Replace(key, value, expires); err != nil { 56 | return ErrNotStored 57 | } 58 | return nil 59 | } 60 | 61 | func (c InMemoryCache) Delete(key string) error { 62 | if found := c.Cache.Delete(key); !found { 63 | return ErrCacheMiss 64 | } 65 | return nil 66 | } 67 | 68 | func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) { 69 | newValue, err = c.Cache.Increment(key, n) 70 | if err == cache.ErrCacheMiss { 71 | return 0, ErrCacheMiss 72 | } 73 | return 74 | } 75 | 76 | func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) { 77 | newValue, err = c.Cache.Decrement(key, n) 78 | if err == cache.ErrCacheMiss { 79 | return 0, ErrCacheMiss 80 | } 81 | return 82 | } 83 | 84 | func (c InMemoryCache) Flush() error { 85 | c.Cache.Flush() 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /flash.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | // Flash represents a cookie that is overwritten on each request. 10 | // It allows data to be stored across one page at a time. 11 | // This is commonly used to implement success or error messages. 12 | // E.g. the Post/Redirect/Get pattern: 13 | // http://en.wikipedia.org/wiki/Post/Redirect/Get 14 | type Flash struct { 15 | // `Data` is the input which is read in `restoreFlash`, `Out` is the output which is set in a FLASH cookie at the end of the `FlashFilter()` 16 | Data, Out map[string]string 17 | } 18 | 19 | // Error serializes the given msg and args to an "error" key within 20 | // the Flash cookie. 21 | func (f Flash) Error(msg string, args ...interface{}) { 22 | if len(args) == 0 { 23 | f.Out["error"] = msg 24 | } else { 25 | f.Out["error"] = fmt.Sprintf(msg, args...) 26 | } 27 | } 28 | 29 | // Success serializes the given msg and args to a "success" key within 30 | // the Flash cookie. 31 | func (f Flash) Success(msg string, args ...interface{}) { 32 | if len(args) == 0 { 33 | f.Out["success"] = msg 34 | } else { 35 | f.Out["success"] = fmt.Sprintf(msg, args...) 36 | } 37 | } 38 | 39 | // FlashFilter is a Revel Filter that retrieves and sets the flash cookie. 40 | // Within Revel, it is available as a Flash attribute on Controller instances. 41 | // The name of the Flash cookie is set as CookiePrefix + "_FLASH". 42 | func FlashFilter(c *Controller, fc []Filter) { 43 | c.Flash = restoreFlash(c.Request.Request) 44 | c.RenderArgs["flash"] = c.Flash.Data 45 | 46 | fc[0](c, fc[1:]) 47 | 48 | // Store the flash. 49 | var flashValue string 50 | for key, value := range c.Flash.Out { 51 | flashValue += "\x00" + key + ":" + value + "\x00" 52 | } 53 | c.SetCookie(&http.Cookie{ 54 | Name: CookiePrefix + "_FLASH", 55 | Value: url.QueryEscape(flashValue), 56 | HttpOnly: true, 57 | Secure: CookieSecure, 58 | Path: "/", 59 | }) 60 | } 61 | 62 | // restoreFlash deserializes a Flash cookie struct from a request. 63 | func restoreFlash(req *http.Request) Flash { 64 | flash := Flash{ 65 | Data: make(map[string]string), 66 | Out: make(map[string]string), 67 | } 68 | if cookie, err := req.Cookie(CookiePrefix + "_FLASH"); err == nil { 69 | ParseKeyValueCookie(cookie.Value, func(key, val string) { 70 | flash.Data[key] = val 71 | }) 72 | } 73 | return flash 74 | } 75 | -------------------------------------------------------------------------------- /cache/serialization_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type Struct1 struct { 9 | X int 10 | } 11 | 12 | func (s Struct1) Method1() {} 13 | 14 | type Interface1 interface { 15 | Method1() 16 | } 17 | 18 | var ( 19 | struct1 Struct1 = Struct1{1} 20 | ptrStruct *Struct1 = &Struct1{2} 21 | emptyIface interface{} = Struct1{3} 22 | iface1 Interface1 = Struct1{4} 23 | sliceStruct []Struct1 = []Struct1{{5}, {6}, {7}} 24 | ptrSliceStruct []*Struct1 = []*Struct1{{8}, {9}, {10}} 25 | 26 | VALUE_MAP = map[string]interface{}{ 27 | "bytes": []byte{0x61, 0x62, 0x63, 0x64}, 28 | "string": "string", 29 | "bool": true, 30 | "int": 5, 31 | "int8": int8(5), 32 | "int16": int16(5), 33 | "int32": int32(5), 34 | "int64": int64(5), 35 | "uint": uint(5), 36 | "uint8": uint8(5), 37 | "uint16": uint16(5), 38 | "uint32": uint32(5), 39 | "uint64": uint64(5), 40 | "float32": float32(5), 41 | "float64": float64(5), 42 | "array": [5]int{1, 2, 3, 4, 5}, 43 | "slice": []int{1, 2, 3, 4, 5}, 44 | "emptyIf": emptyIface, 45 | "Iface1": iface1, 46 | "map": map[string]string{"foo": "bar"}, 47 | "ptrStruct": ptrStruct, 48 | "struct1": struct1, 49 | "sliceStruct": sliceStruct, 50 | "ptrSliceStruct": ptrSliceStruct, 51 | } 52 | ) 53 | 54 | // Test passing all kinds of data between serialize and deserialize. 55 | func TestRoundTrip(t *testing.T) { 56 | for _, expected := range VALUE_MAP { 57 | bytes, err := Serialize(expected) 58 | if err != nil { 59 | t.Error(err) 60 | continue 61 | } 62 | 63 | ptrActual := reflect.New(reflect.TypeOf(expected)).Interface() 64 | err = Deserialize(bytes, ptrActual) 65 | if err != nil { 66 | t.Error(err) 67 | continue 68 | } 69 | 70 | actual := reflect.ValueOf(ptrActual).Elem().Interface() 71 | if !reflect.DeepEqual(expected, actual) { 72 | t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual) 73 | } 74 | } 75 | } 76 | 77 | func zeroMap(arg map[string]interface{}) map[string]interface{} { 78 | result := map[string]interface{}{} 79 | for key, value := range arg { 80 | result[key] = reflect.Zero(reflect.TypeOf(value)).Interface() 81 | } 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /cache/serialization.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/revel/revel" 7 | "reflect" 8 | "strconv" 9 | ) 10 | 11 | // Serialize transforms the given value into bytes following these rules: 12 | // - If value is a byte array, it is returned as-is. 13 | // - If value is an int or uint type, it is returned as the ASCII representation 14 | // - Else, encoding/gob is used to serialize 15 | func Serialize(value interface{}) ([]byte, error) { 16 | if bytes, ok := value.([]byte); ok { 17 | return bytes, nil 18 | } 19 | 20 | switch v := reflect.ValueOf(value); v.Kind() { 21 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 22 | return []byte(strconv.FormatInt(v.Int(), 10)), nil 23 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 24 | return []byte(strconv.FormatUint(v.Uint(), 10)), nil 25 | } 26 | 27 | var b bytes.Buffer 28 | encoder := gob.NewEncoder(&b) 29 | if err := encoder.Encode(value); err != nil { 30 | revel.ERROR.Printf("revel/cache: gob encoding '%s' failed: %s", value, err) 31 | return nil, err 32 | } 33 | return b.Bytes(), nil 34 | } 35 | 36 | // Deserialize transforms bytes produced by Serialize back into a Go object, 37 | // storing it into "ptr", which must be a pointer to the value type. 38 | func Deserialize(byt []byte, ptr interface{}) (err error) { 39 | if bytes, ok := ptr.(*[]byte); ok { 40 | *bytes = byt 41 | return 42 | } 43 | 44 | if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr { 45 | switch p := v.Elem(); p.Kind() { 46 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 47 | var i int64 48 | i, err = strconv.ParseInt(string(byt), 10, 64) 49 | if err != nil { 50 | revel.ERROR.Printf("revel/cache: failed to parse int '%s': %s", string(byt), err) 51 | } else { 52 | p.SetInt(i) 53 | } 54 | return 55 | 56 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 57 | var i uint64 58 | i, err = strconv.ParseUint(string(byt), 10, 64) 59 | if err != nil { 60 | revel.ERROR.Printf("revel/cache: failed to parse uint '%s': %s", string(byt), err) 61 | } else { 62 | p.SetUint(i) 63 | } 64 | return 65 | } 66 | } 67 | 68 | b := bytes.NewBuffer(byt) 69 | decoder := gob.NewDecoder(b) 70 | if err = decoder.Decode(ptr); err != nil { 71 | revel.ERROR.Printf("revel/cache: gob decoding failed: %s", err) 72 | return 73 | } 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | // getRecordedCookie returns the recorded cookie from a ResponseRecorder with 10 | // the given name. It utilizes the cookie reader found in the standard library. 11 | func getRecordedCookie(recorder *httptest.ResponseRecorder, name string) (*http.Cookie, error) { 12 | r := &http.Response{Header: recorder.HeaderMap} 13 | for _, cookie := range r.Cookies() { 14 | if cookie.Name == name { 15 | return cookie, nil 16 | } 17 | } 18 | return nil, http.ErrNoCookie 19 | } 20 | 21 | func validationTester(req *Request, fn func(c *Controller)) *httptest.ResponseRecorder { 22 | recorder := httptest.NewRecorder() 23 | c := NewController(req, NewResponse(recorder)) 24 | ValidationFilter(c, []Filter{func(c *Controller, _ []Filter) { 25 | fn(c) 26 | }}) 27 | return recorder 28 | } 29 | 30 | // Test that errors are encoded into the _ERRORS cookie. 31 | func TestValidationWithError(t *testing.T) { 32 | recorder := validationTester(buildEmptyRequest(), func(c *Controller) { 33 | c.Validation.Required("") 34 | if !c.Validation.HasErrors() { 35 | t.Fatal("errors should be present") 36 | } 37 | c.Validation.Keep() 38 | }) 39 | 40 | if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil { 41 | t.Fatal(err) 42 | } else if cookie.MaxAge < 0 { 43 | t.Fatalf("cookie should not expire") 44 | } 45 | } 46 | 47 | // Test that no cookie is sent if errors are found, but Keep() is not called. 48 | func TestValidationNoKeep(t *testing.T) { 49 | recorder := validationTester(buildEmptyRequest(), func(c *Controller) { 50 | c.Validation.Required("") 51 | if !c.Validation.HasErrors() { 52 | t.Fatal("errors should not be present") 53 | } 54 | }) 55 | 56 | if _, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != http.ErrNoCookie { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | // Test that a previously set _ERRORS cookie is deleted if no errors are found. 62 | func TestValidationNoKeepCookiePreviouslySet(t *testing.T) { 63 | req := buildRequestWithCookie("REVEL_ERRORS", "invalid") 64 | recorder := validationTester(req, func(c *Controller) { 65 | c.Validation.Required("success") 66 | if c.Validation.HasErrors() { 67 | t.Fatal("errors should not be present") 68 | } 69 | }) 70 | 71 | if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil { 72 | t.Fatal(err) 73 | } else if cookie.MaxAge >= 0 { 74 | t.Fatalf("cookie should be deleted") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - tip 8 | 9 | os: 10 | - linux 11 | - osx 12 | 13 | sudo: false 14 | 15 | services: 16 | # github.com/revel/revel/cache 17 | - memcache 18 | - redis-server 19 | 20 | before_install: 21 | # TRAVIS_OS_NAME - linux and osx 22 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install memcached redis; fi' 23 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then nohup /usr/local/opt/memcached/bin/memcached & fi' 24 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then nohup redis-server /usr/local/etc/redis.conf & fi' 25 | 26 | install: 27 | # Setting environments variables 28 | - export PATH=$PATH:$HOME/gopath/bin 29 | - export REVEL_BRANCH="develop" 30 | - 'if [[ "$TRAVIS_BRANCH" == "master" ]]; then export REVEL_BRANCH="master"; fi' 31 | - 'echo "Travis branch: $TRAVIS_BRANCH, Revel dependency branch: $REVEL_BRANCH"' 32 | - go get -v github.com/revel/revel/... 33 | - rm -rf ../config 34 | - git clone -b $REVEL_BRANCH git://github.com/revel/modules ../modules/ 35 | - git clone -b $REVEL_BRANCH git://github.com/revel/cmd ../cmd/ 36 | - git clone -b $REVEL_BRANCH git://github.com/revel/config ../config/ 37 | - git clone -b $REVEL_BRANCH git://github.com/revel/cron ../cron/ 38 | - git clone git://github.com/revel/samples ../samples/ 39 | - go get -v github.com/revel/cmd/revel 40 | 41 | script: 42 | - go test -v github.com/revel/revel... 43 | 44 | # Ensure the new-app flow works (plus the other commands). 45 | - revel version 46 | - revel new my/testapp 47 | - revel test my/testapp 48 | - revel clean my/testapp 49 | - revel build my/testapp build/testapp 50 | - revel build my/testapp build/testapp prod 51 | - revel package my/testapp 52 | - revel package my/testapp prod 53 | 54 | # Build & run the sample apps 55 | - revel test github.com/revel/samples/booking 56 | - revel test github.com/revel/samples/chat 57 | - revel test github.com/revel/samples/facebook-oauth2 58 | - revel test github.com/revel/samples/twitter-oauth 59 | - revel test github.com/revel/samples/validation 60 | - revel test github.com/revel/samples/upload 61 | 62 | # Commented out persona test sample, since persona.org gonna be shutdown. 63 | # Also http://personatestuser.org becomes non-responsive most of the time. 64 | # https://wiki.mozilla.org/Identity/Persona_Shutdown_Guidelines_for_Reliers 65 | # - revel test github.com/revel/samples/persona 66 | 67 | matrix: 68 | allow_failures: 69 | - go: tip 70 | -------------------------------------------------------------------------------- /fakeapp_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path" 8 | "reflect" 9 | ) 10 | 11 | type Hotel struct { 12 | HotelId int 13 | Name, Address string 14 | City, State, Zip string 15 | Country string 16 | Price int 17 | } 18 | 19 | type Hotels struct { 20 | *Controller 21 | } 22 | 23 | type Static struct { 24 | *Controller 25 | } 26 | 27 | func (c Hotels) Show(id int) Result { 28 | title := "View Hotel" 29 | hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300} 30 | return c.Render(title, hotel) 31 | } 32 | 33 | func (c Hotels) Book(id int) Result { 34 | hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300} 35 | return c.RenderJson(hotel) 36 | } 37 | 38 | func (c Hotels) Index() Result { 39 | return c.RenderText("Hello, World!") 40 | } 41 | 42 | func (c Static) Serve(prefix, filepath string) Result { 43 | var basePath, dirName string 44 | 45 | if !path.IsAbs(dirName) { 46 | basePath = BasePath 47 | } 48 | 49 | fname := path.Join(basePath, prefix, filepath) 50 | file, err := os.Open(fname) 51 | if os.IsNotExist(err) { 52 | return c.NotFound("") 53 | } else if err != nil { 54 | WARN.Printf("Problem opening file (%s): %s ", fname, err) 55 | return c.NotFound("This was found but not sure why we couldn't open it.") 56 | } 57 | return c.RenderFile(file, "") 58 | } 59 | 60 | func startFakeBookingApp() { 61 | Init("prod", "github.com/revel/revel/testdata", "") 62 | 63 | // Disable logging. 64 | TRACE = log.New(ioutil.Discard, "", 0) 65 | INFO = TRACE 66 | WARN = TRACE 67 | ERROR = TRACE 68 | 69 | MainTemplateLoader = NewTemplateLoader([]string{ViewsPath, path.Join(RevelPath, "templates")}) 70 | MainTemplateLoader.Refresh() 71 | 72 | RegisterController((*Hotels)(nil), 73 | []*MethodType{ 74 | &MethodType{ 75 | Name: "Index", 76 | }, 77 | &MethodType{ 78 | Name: "Show", 79 | Args: []*MethodArg{ 80 | {"id", reflect.TypeOf((*int)(nil))}, 81 | }, 82 | RenderArgNames: map[int][]string{30: []string{"title", "hotel"}}, 83 | }, 84 | &MethodType{ 85 | Name: "Book", 86 | Args: []*MethodArg{ 87 | {"id", reflect.TypeOf((*int)(nil))}, 88 | }, 89 | }, 90 | }) 91 | 92 | RegisterController((*Static)(nil), 93 | []*MethodType{ 94 | &MethodType{ 95 | Name: "Serve", 96 | Args: []*MethodArg{ 97 | &MethodArg{Name: "prefix", Type: reflect.TypeOf((*string)(nil))}, 98 | &MethodArg{Name: "filepath", Type: reflect.TypeOf((*string)(nil))}, 99 | }, 100 | RenderArgNames: map[int][]string{}, 101 | }, 102 | }) 103 | 104 | runStartupHooks() 105 | } 106 | -------------------------------------------------------------------------------- /skeleton/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Revel 2 | 3 | ## Getting Started 4 | 5 | A high-productivity web framework for the [Go language](http://www.golang.org/). 6 | 7 | ### Start the web server: 8 | 9 | revel run myapp 10 | 11 | Run with --help for options. 12 | 13 | ### Go to http://localhost:9000/ and you'll see: 14 | 15 | "It works" 16 | 17 | ### Description of Contents 18 | 19 | The default directory structure of a generated Revel application: 20 | 21 | myapp App root 22 | app App sources 23 | controllers App controllers 24 | init.go Interceptor registration 25 | models App domain models 26 | routes Reverse routes (generated code) 27 | views Templates 28 | tests Test suites 29 | conf Configuration files 30 | app.conf Main configuration file 31 | routes Routes definition 32 | messages Message files 33 | public Public assets 34 | css CSS files 35 | js Javascript files 36 | images Image files 37 | 38 | app 39 | 40 | The app directory contains the source code and templates for your application. 41 | 42 | conf 43 | 44 | The conf directory contains the application’s configuration files. There are two main configuration files: 45 | 46 | * app.conf, the main configuration file for the application, which contains standard configuration parameters 47 | * routes, the routes definition file. 48 | 49 | 50 | messages 51 | 52 | The messages directory contains all localized message files. 53 | 54 | public 55 | 56 | Resources stored in the public directory are static assets that are served directly by the Web server. Typically it is split into three standard sub-directories for images, CSS stylesheets and JavaScript files. 57 | 58 | The names of these directories may be anything; the developer need only update the routes. 59 | 60 | test 61 | 62 | Tests are kept in the tests directory. Revel provides a testing framework that makes it easy to write and run functional tests against your application. 63 | 64 | ### Follow the guidelines to start developing your application: 65 | 66 | * The README file created within your application. 67 | * The [Getting Started with Revel](http://revel.github.io/tutorial/index.html). 68 | * The [Revel guides](http://revel.github.io/manual/index.html). 69 | * The [Revel sample apps](http://revel.github.io/samples/index.html). 70 | * The [API documentation](https://godoc.org/github.com/revel/revel). 71 | 72 | ## Contributing 73 | We encourage you to contribute to Revel! Please check out the [Contributing to Revel 74 | guide](https://github.com/revel/revel/blob/master/CONTRIBUTING.md) for guidelines about how 75 | to proceed. [Join us](https://groups.google.com/forum/#!forum/revel-framework)! 76 | -------------------------------------------------------------------------------- /intercept_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var funcP = func(c *Controller) Result { return nil } 9 | var funcP2 = func(c *Controller) Result { return nil } 10 | 11 | type InterceptController struct{ *Controller } 12 | type InterceptControllerN struct{ InterceptController } 13 | type InterceptControllerP struct{ *InterceptController } 14 | type InterceptControllerNP struct { 15 | *Controller 16 | InterceptControllerN 17 | InterceptControllerP 18 | } 19 | 20 | func (c InterceptController) methN() Result { return nil } 21 | func (c *InterceptController) methP() Result { return nil } 22 | 23 | func (c InterceptControllerN) methNN() Result { return nil } 24 | func (c *InterceptControllerN) methNP() Result { return nil } 25 | func (c InterceptControllerP) methPN() Result { return nil } 26 | func (c *InterceptControllerP) methPP() Result { return nil } 27 | 28 | // Methods accessible from InterceptControllerN 29 | var METHODS_N = []interface{}{ 30 | InterceptController.methN, 31 | (*InterceptController).methP, 32 | InterceptControllerN.methNN, 33 | (*InterceptControllerN).methNP, 34 | } 35 | 36 | // Methods accessible from InterceptControllerP 37 | var METHODS_P = []interface{}{ 38 | InterceptController.methN, 39 | (*InterceptController).methP, 40 | InterceptControllerP.methPN, 41 | (*InterceptControllerP).methPP, 42 | } 43 | 44 | // This checks that all the various kinds of interceptor functions/methods are 45 | // properly invoked. 46 | func TestInvokeArgType(t *testing.T) { 47 | n := InterceptControllerN{InterceptController{&Controller{}}} 48 | p := InterceptControllerP{&InterceptController{&Controller{}}} 49 | np := InterceptControllerNP{&Controller{}, n, p} 50 | testInterceptorController(t, reflect.ValueOf(&n), METHODS_N) 51 | testInterceptorController(t, reflect.ValueOf(&p), METHODS_P) 52 | testInterceptorController(t, reflect.ValueOf(&np), METHODS_N) 53 | testInterceptorController(t, reflect.ValueOf(&np), METHODS_P) 54 | } 55 | 56 | func testInterceptorController(t *testing.T, appControllerPtr reflect.Value, methods []interface{}) { 57 | interceptors = []*Interception{} 58 | InterceptFunc(funcP, BEFORE, appControllerPtr.Elem().Interface()) 59 | InterceptFunc(funcP2, BEFORE, ALL_CONTROLLERS) 60 | for _, m := range methods { 61 | InterceptMethod(m, BEFORE) 62 | } 63 | ints := getInterceptors(BEFORE, appControllerPtr) 64 | 65 | if len(ints) != 6 { 66 | t.Fatalf("N: Expected 6 interceptors, got %d.", len(ints)) 67 | } 68 | 69 | testInterception(t, ints[0], reflect.ValueOf(&Controller{})) 70 | testInterception(t, ints[1], reflect.ValueOf(&Controller{})) 71 | for i := range methods { 72 | testInterception(t, ints[i+2], appControllerPtr) 73 | } 74 | } 75 | 76 | func testInterception(t *testing.T, intc *Interception, arg reflect.Value) { 77 | val := intc.Invoke(arg) 78 | if !val.IsNil() { 79 | t.Errorf("Failed (%s): Expected nil got %v", intc, val) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestContentTypeByFilename(t *testing.T) { 11 | testCases := map[string]string{ 12 | "xyz.jpg": "image/jpeg", 13 | "helloworld.c": "text/x-c; charset=utf-8", 14 | "helloworld.": "application/octet-stream", 15 | "helloworld": "application/octet-stream", 16 | "hello.world.c": "text/x-c; charset=utf-8", 17 | } 18 | srcPath, _ := findSrcPaths(REVEL_IMPORT_PATH) 19 | ConfPaths = []string{path.Join( 20 | srcPath, 21 | filepath.FromSlash(REVEL_IMPORT_PATH), 22 | "conf"), 23 | } 24 | LoadMimeConfig() 25 | for filename, expected := range testCases { 26 | actual := ContentTypeByFilename(filename) 27 | if actual != expected { 28 | t.Errorf("%s: %s, Expected %s", filename, actual, expected) 29 | } 30 | } 31 | } 32 | 33 | func TestEqual(t *testing.T) { 34 | type testStruct struct{} 35 | type testStruct2 struct{} 36 | i, i2 := 8, 9 37 | s, s2 := "@朕µ\n\tüöäß", "@朕µ\n\tüöäss" 38 | slice, slice2 := []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5} 39 | slice3, slice4 := []int{5, 4, 3, 2, 1}, []int{5, 4, 3, 2, 1} 40 | 41 | tm := map[string][]interface{}{ 42 | "slices": {slice, slice2}, 43 | "slices2": {slice3, slice4}, 44 | "types": {new(testStruct), new(testStruct)}, 45 | "types2": {new(testStruct2), new(testStruct2)}, 46 | "ints": {int(i), int8(i), int16(i), int32(i), int64(i)}, 47 | "ints2": {int(i2), int8(i2), int16(i2), int32(i2), int64(i2)}, 48 | "uints": {uint(i), uint8(i), uint16(i), uint32(i), uint64(i)}, 49 | "uints2": {uint(i2), uint8(i2), uint16(i2), uint32(i2), uint64(i2)}, 50 | "floats": {float32(i), float64(i)}, 51 | "floats2": {float32(i2), float64(i2)}, 52 | "strings": {[]byte(s), s}, 53 | "strings2": {[]byte(s2), s2}, 54 | } 55 | 56 | testRow := func(row, row2 string, expected bool) { 57 | for _, a := range tm[row] { 58 | for _, b := range tm[row2] { 59 | ok := Equal(a, b) 60 | if ok != expected { 61 | ak := reflect.TypeOf(a).Kind() 62 | bk := reflect.TypeOf(b).Kind() 63 | t.Errorf("eq(%s=%v,%s=%v) want %t got %t", ak, a, bk, b, expected, ok) 64 | } 65 | } 66 | } 67 | } 68 | 69 | testRow("slices", "slices", true) 70 | testRow("slices", "slices2", false) 71 | testRow("slices2", "slices", false) 72 | 73 | testRow("types", "types", true) 74 | testRow("types2", "types", false) 75 | testRow("types", "types2", false) 76 | 77 | testRow("ints", "ints", true) 78 | testRow("ints", "ints2", false) 79 | testRow("ints2", "ints", false) 80 | 81 | testRow("uints", "uints", true) 82 | testRow("uints2", "uints", false) 83 | testRow("uints", "uints2", false) 84 | 85 | testRow("floats", "floats", true) 86 | testRow("floats2", "floats", false) 87 | testRow("floats", "floats2", false) 88 | 89 | testRow("strings", "strings", true) 90 | testRow("strings2", "strings", false) 91 | testRow("strings", "strings2", false) 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Revel Framework 2 | 3 | [![Build Status](https://secure.travis-ci.org/revel/revel.svg?branch=master)](http://travis-ci.org/revel/revel) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | 5 | A high productivity, full-stack web framework for the [Go language](http://www.golang.org). 6 | 7 | Current Version: 0.13.1 (2016-06-06) 8 | 9 | **As of Revel 0.13.0, Go 1.4+ is required.** 10 | 11 | ## Quick Start 12 | 13 | Install revel framework: 14 | 15 | go get -u github.com/revel/cmd/revel 16 | 17 | Create a new app and run it: 18 | 19 | revel new github.com/myaccount/my-app 20 | revel run github.com/myaccount/my-app 21 | 22 | Open http://localhost:9000 in your browser and you should see "It works!" 23 | 24 | ## Learn More 25 | 26 | * [Manual, Samples, Godocs, etc](http://revel.github.io) 27 | * [Apps using Revel](https://github.com/revel/revel/wiki/Apps-in-the-Wild) 28 | * [Articles Featuring Revel](https://github.com/revel/revel/wiki/Articles) 29 | 30 | ## Contributing 31 | 32 | * [Contributing Code Guidelines](https://github.com/revel/revel/blob/master/CONTRIBUTING.md) 33 | * [Revel Contributors](https://github.com/revel/revel/graphs/contributors) 34 | 35 | ## Community 36 | 37 | Join on [StackOverflow](http://stackoverflow.com/questions/tagged/revel), [IRC #revel](http://webchat.freenode.net/?channels=%23revel&uio=d4) and [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) 38 | 39 | * Report bugs [here](https://github.com/revel/revel/issues) 40 | * Answer questions of other community members (via [StackOverflow](http://stackoverflow.com/questions/tagged/revel), [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) and [IRC](http://webchat.freenode.net/?channels=%23revel&uio=d4)) 41 | * Give feedback on new feature discussions (via [GitHub Issues](https://github.com/revel/revel/issues) and [Google Groups](https://groups.google.com/forum/#!forum/revel-framework)) 42 | * Propose your own ideas via [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) 43 | 44 | 45 | ## Gratitude 46 | 47 | First and foremost, we'd like to thank the growing community of developers who enjoy using and contributing to Revel. Your patience, feedback, and moral support are vital to the ongoing development of Revel! 48 | 49 | Also, thank you to those who have increased their level of involvement with Revel and revitalized the momentum of Revel: 50 | * [Jeeva](https://github.com/jeevatkm) 51 | * [Pedro](https://github.com/pedromorgan) 52 | * Many others who provided valuable Pull Requests, testing, and feedback. 53 | 54 | Finally, we'd like to thank the professional organizations that have supported the development of Revel: 55 | * [Looking Glass](https://www.lookingglasscyber.com/) 56 | * [Surge](http://surgeforward.com/) 57 | 58 | 59 | ## Announcements 60 | 61 | View the [v0.13.0 release notes](https://github.com/revel/revel/releases/tag/v0.13.0) 62 | for all of the relevant changes. 63 | 64 | We are working on increasing the speed and quality of our releases. Your feedback has never been so valuable, please share your thoughts with us and help shape Revel! 65 | -------------------------------------------------------------------------------- /templates/errors/500-dev.html: -------------------------------------------------------------------------------- 1 | 93 | {{with .Error}} 94 | 104 | {{if .Path}} 105 |
106 |

In {{.Path}} 107 | {{if .Line}} 108 | (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}}) 109 | {{end}} 110 |

111 | {{range .ContextSource}} 112 |
113 | {{.Line}}: 114 |
{{.Source}}
115 |
116 | {{end}} 117 |
118 | {{end}} 119 | {{if .Stack}} 120 |
121 |

Call Stack

122 | {{.Stack}} 123 |
124 | {{end}} 125 | {{if .MetaError}} 126 |
127 |

Additionally, an error occurred while handling this error.

128 |
129 | {{.MetaError}} 130 |
131 |
132 | {{end}} 133 | {{end}} 134 | -------------------------------------------------------------------------------- /cache/memcached.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "github.com/bradfitz/gomemcache/memcache" 6 | "github.com/revel/revel" 7 | "time" 8 | ) 9 | 10 | // Wraps the Memcached client to meet the Cache interface. 11 | type MemcachedCache struct { 12 | *memcache.Client 13 | defaultExpiration time.Duration 14 | } 15 | 16 | func NewMemcachedCache(hostList []string, defaultExpiration time.Duration) MemcachedCache { 17 | return MemcachedCache{memcache.New(hostList...), defaultExpiration} 18 | } 19 | 20 | func (c MemcachedCache) Set(key string, value interface{}, expires time.Duration) error { 21 | return c.invoke((*memcache.Client).Set, key, value, expires) 22 | } 23 | 24 | func (c MemcachedCache) Add(key string, value interface{}, expires time.Duration) error { 25 | return c.invoke((*memcache.Client).Add, key, value, expires) 26 | } 27 | 28 | func (c MemcachedCache) Replace(key string, value interface{}, expires time.Duration) error { 29 | return c.invoke((*memcache.Client).Replace, key, value, expires) 30 | } 31 | 32 | func (c MemcachedCache) Get(key string, ptrValue interface{}) error { 33 | item, err := c.Client.Get(key) 34 | if err != nil { 35 | return convertMemcacheError(err) 36 | } 37 | return Deserialize(item.Value, ptrValue) 38 | } 39 | 40 | func (c MemcachedCache) GetMulti(keys ...string) (Getter, error) { 41 | items, err := c.Client.GetMulti(keys) 42 | if err != nil { 43 | return nil, convertMemcacheError(err) 44 | } 45 | return ItemMapGetter(items), nil 46 | } 47 | 48 | func (c MemcachedCache) Delete(key string) error { 49 | return convertMemcacheError(c.Client.Delete(key)) 50 | } 51 | 52 | func (c MemcachedCache) Increment(key string, delta uint64) (newValue uint64, err error) { 53 | newValue, err = c.Client.Increment(key, delta) 54 | return newValue, convertMemcacheError(err) 55 | } 56 | 57 | func (c MemcachedCache) Decrement(key string, delta uint64) (newValue uint64, err error) { 58 | newValue, err = c.Client.Decrement(key, delta) 59 | return newValue, convertMemcacheError(err) 60 | } 61 | 62 | func (c MemcachedCache) Flush() error { 63 | err := errors.New("revel/cache: can not flush memcached.") 64 | revel.ERROR.Println(err) 65 | return err 66 | } 67 | 68 | func (c MemcachedCache) invoke(f func(*memcache.Client, *memcache.Item) error, 69 | key string, value interface{}, expires time.Duration) error { 70 | 71 | switch expires { 72 | case DEFAULT: 73 | expires = c.defaultExpiration 74 | case FOREVER: 75 | expires = time.Duration(0) 76 | } 77 | 78 | b, err := Serialize(value) 79 | if err != nil { 80 | return err 81 | } 82 | return convertMemcacheError(f(c.Client, &memcache.Item{ 83 | Key: key, 84 | Value: b, 85 | Expiration: int32(expires / time.Second), 86 | })) 87 | } 88 | 89 | // Implement a Getter on top of the returned item map. 90 | type ItemMapGetter map[string]*memcache.Item 91 | 92 | func (g ItemMapGetter) Get(key string, ptrValue interface{}) error { 93 | item, ok := g[key] 94 | if !ok { 95 | return ErrCacheMiss 96 | } 97 | 98 | return Deserialize(item.Value, ptrValue) 99 | } 100 | 101 | func convertMemcacheError(err error) error { 102 | switch err { 103 | case nil: 104 | return nil 105 | case memcache.ErrCacheMiss: 106 | return ErrCacheMiss 107 | case memcache.ErrNotStored: 108 | return ErrNotStored 109 | } 110 | 111 | revel.ERROR.Println("revel/cache:", err) 112 | return err 113 | } 114 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "path" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | // This tries to benchmark the usual request-serving pipeline to get an overall 13 | // performance metric. 14 | // 15 | // Each iteration runs one mock request to display a hotel's detail page by id. 16 | // 17 | // Contributing parts: 18 | // - Routing 19 | // - Controller lookup / invocation 20 | // - Parameter binding 21 | // - Session, flash, i18n cookies 22 | // - Render() call magic 23 | // - Template rendering 24 | func BenchmarkServeAction(b *testing.B) { 25 | benchmarkRequest(b, showRequest) 26 | } 27 | 28 | func BenchmarkServeJson(b *testing.B) { 29 | benchmarkRequest(b, jsonRequest) 30 | } 31 | 32 | func BenchmarkServePlaintext(b *testing.B) { 33 | benchmarkRequest(b, plaintextRequest) 34 | } 35 | 36 | // This tries to benchmark the static serving overhead when serving an "average 37 | // size" 7k file. 38 | func BenchmarkServeStatic(b *testing.B) { 39 | benchmarkRequest(b, staticRequest) 40 | } 41 | 42 | func benchmarkRequest(b *testing.B, req *http.Request) { 43 | startFakeBookingApp() 44 | b.ResetTimer() 45 | resp := httptest.NewRecorder() 46 | for i := 0; i < b.N; i++ { 47 | handle(resp, req) 48 | } 49 | } 50 | 51 | // Test that the booking app can be successfully run for a test. 52 | func TestFakeServer(t *testing.T) { 53 | startFakeBookingApp() 54 | 55 | resp := httptest.NewRecorder() 56 | 57 | // First, test that the expected responses are actually generated 58 | handle(resp, showRequest) 59 | if !strings.Contains(resp.Body.String(), "300 Main St.") { 60 | t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body) 61 | t.FailNow() 62 | } 63 | resp.Body.Reset() 64 | 65 | handle(resp, staticRequest) 66 | sessvarsSize := getFileSize(t, path.Join(BasePath, "public", "js", "sessvars.js")) 67 | if int64(resp.Body.Len()) != sessvarsSize { 68 | t.Errorf("Expected sessvars.js to have %d bytes, got %d:\n%s", sessvarsSize, resp.Body.Len(), resp.Body) 69 | t.FailNow() 70 | } 71 | resp.Body.Reset() 72 | 73 | handle(resp, jsonRequest) 74 | if !strings.Contains(resp.Body.String(), `"Address":"300 Main St."`) { 75 | t.Errorf("Failed to find hotel address in JSON response:\n%s", resp.Body) 76 | t.FailNow() 77 | } 78 | resp.Body.Reset() 79 | 80 | handle(resp, plaintextRequest) 81 | if resp.Body.String() != "Hello, World!" { 82 | t.Errorf("Failed to find greeting in plaintext response:\n%s", resp.Body) 83 | t.FailNow() 84 | } 85 | 86 | resp.Body = nil 87 | } 88 | 89 | func getFileSize(t *testing.T, name string) int64 { 90 | fi, err := os.Stat(name) 91 | if err != nil { 92 | t.Errorf("Unable to stat file:\n%s", name) 93 | t.FailNow() 94 | } 95 | return fi.Size() 96 | } 97 | 98 | func TestOnAppStart(t *testing.T) { 99 | str := "" 100 | OnAppStart(func() { 101 | str += " World" 102 | }, 2) 103 | 104 | OnAppStart(func() { 105 | str += "Hello" 106 | }, 1) 107 | 108 | startFakeBookingApp() 109 | if str != "Hello World" { 110 | t.Errorf("Failed to order OnAppStart:\n%s", str) 111 | t.FailNow() 112 | } 113 | } 114 | 115 | var ( 116 | showRequest, _ = http.NewRequest("GET", "/hotels/3", nil) 117 | staticRequest, _ = http.NewRequest("GET", "/public/js/sessvars.js", nil) 118 | jsonRequest, _ = http.NewRequest("GET", "/hotels/3/booking", nil) 119 | plaintextRequest, _ = http.NewRequest("GET", "/hotels", nil) 120 | ) 121 | -------------------------------------------------------------------------------- /invoker_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // These tests verify that Controllers are initialized properly, given the range 10 | // of embedding possibilities.. 11 | 12 | type P struct{ *Controller } 13 | 14 | type PN struct{ P } 15 | 16 | type PNN struct{ PN } 17 | 18 | // Embedded via two paths 19 | type P2 struct{ *Controller } 20 | type PP2 struct { 21 | *Controller // Need to embed this explicitly to avoid duplicate selector. 22 | P 23 | P2 24 | PNN 25 | } 26 | 27 | var GENERATIONS = []interface{}{P{}, PN{}, PNN{}} 28 | 29 | func TestFindControllers(t *testing.T) { 30 | controllers = make(map[string]*ControllerType) 31 | RegisterController((*P)(nil), nil) 32 | RegisterController((*PN)(nil), nil) 33 | RegisterController((*PNN)(nil), nil) 34 | RegisterController((*PP2)(nil), nil) 35 | 36 | // Test construction of indexes to each *Controller 37 | checkSearchResults(t, P{}, [][]int{{0}}) 38 | checkSearchResults(t, PN{}, [][]int{{0, 0}}) 39 | checkSearchResults(t, PNN{}, [][]int{{0, 0, 0}}) 40 | checkSearchResults(t, PP2{}, [][]int{{0}, {1, 0}, {2, 0}, {3, 0, 0, 0}}) 41 | } 42 | 43 | func checkSearchResults(t *testing.T, obj interface{}, expected [][]int) { 44 | actual := findControllers(reflect.TypeOf(obj)) 45 | if !reflect.DeepEqual(expected, actual) { 46 | t.Errorf("Indexes do not match. expected %v actual %v", expected, actual) 47 | } 48 | } 49 | 50 | func TestSetAction(t *testing.T) { 51 | controllers = make(map[string]*ControllerType) 52 | RegisterController((*P)(nil), []*MethodType{{Name: "Method"}}) 53 | RegisterController((*PNN)(nil), []*MethodType{{Name: "Method"}}) 54 | RegisterController((*PP2)(nil), []*MethodType{{Name: "Method"}}) 55 | 56 | // Test that all *revel.Controllers are initialized. 57 | c := &Controller{Name: "Test"} 58 | if err := c.SetAction("P", "Method"); err != nil { 59 | t.Error(err) 60 | } else if c.AppController.(*P).Controller != c { 61 | t.Errorf("P not initialized") 62 | } 63 | 64 | if err := c.SetAction("PNN", "Method"); err != nil { 65 | t.Error(err) 66 | } else if c.AppController.(*PNN).Controller != c { 67 | t.Errorf("PNN not initialized") 68 | } 69 | 70 | // PP2 has 4 different slots for *Controller. 71 | if err := c.SetAction("PP2", "Method"); err != nil { 72 | t.Error(err) 73 | } else if pp2 := c.AppController.(*PP2); pp2.Controller != c || 74 | pp2.P.Controller != c || 75 | pp2.P2.Controller != c || 76 | pp2.PNN.Controller != c { 77 | t.Errorf("PP2 not initialized") 78 | } 79 | } 80 | 81 | func BenchmarkSetAction(b *testing.B) { 82 | type Mixin1 struct { 83 | *Controller 84 | x, y int 85 | foo string 86 | } 87 | type Mixin2 struct { 88 | *Controller 89 | a, b float64 90 | bar string 91 | } 92 | 93 | type Benchmark struct { 94 | *Controller 95 | Mixin1 96 | Mixin2 97 | user interface{} 98 | guy string 99 | } 100 | 101 | RegisterController((*Mixin1)(nil), []*MethodType{{Name: "Method"}}) 102 | RegisterController((*Mixin2)(nil), []*MethodType{{Name: "Method"}}) 103 | RegisterController((*Benchmark)(nil), []*MethodType{{Name: "Method"}}) 104 | c := Controller{ 105 | RenderArgs: make(map[string]interface{}), 106 | } 107 | 108 | for i := 0; i < b.N; i++ { 109 | if err := c.SetAction("Benchmark", "Method"); err != nil { 110 | b.Errorf("Failed to set action: %s", err) 111 | return 112 | } 113 | } 114 | } 115 | 116 | func BenchmarkInvoker(b *testing.B) { 117 | startFakeBookingApp() 118 | c := Controller{ 119 | RenderArgs: make(map[string]interface{}), 120 | } 121 | if err := c.SetAction("Hotels", "Show"); err != nil { 122 | b.Errorf("Failed to set action: %s", err) 123 | return 124 | } 125 | c.Request = NewRequest(showRequest) 126 | c.Params = &Params{Values: make(url.Values)} 127 | c.Params.Set("id", "3") 128 | 129 | b.ResetTimer() 130 | for i := 0; i < b.N; i++ { 131 | ActionInvoker(&c, nil) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /filterconfig_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import "testing" 4 | 5 | type FakeController struct{} 6 | 7 | func (c FakeController) Foo() {} 8 | func (c *FakeController) Bar() {} 9 | 10 | func TestFilterConfiguratorKey(t *testing.T) { 11 | conf := FilterController(FakeController{}) 12 | if conf.key != "FakeController" { 13 | t.Errorf("Expected key 'FakeController', was %s", conf.key) 14 | } 15 | 16 | conf = FilterController(&FakeController{}) 17 | if conf.key != "FakeController" { 18 | t.Errorf("Expected key 'FakeController', was %s", conf.key) 19 | } 20 | 21 | conf = FilterAction(FakeController.Foo) 22 | if conf.key != "FakeController.Foo" { 23 | t.Errorf("Expected key 'FakeController.Foo', was %s", conf.key) 24 | } 25 | 26 | conf = FilterAction((*FakeController).Bar) 27 | if conf.key != "FakeController.Bar" { 28 | t.Errorf("Expected key 'FakeController.Bar', was %s", conf.key) 29 | } 30 | } 31 | 32 | func TestFilterConfigurator(t *testing.T) { 33 | // Filters is global state. Restore it after this test. 34 | oldFilters := make([]Filter, len(Filters)) 35 | copy(oldFilters, Filters) 36 | defer func() { 37 | Filters = oldFilters 38 | }() 39 | 40 | Filters = []Filter{ 41 | RouterFilter, 42 | FilterConfiguringFilter, 43 | SessionFilter, 44 | FlashFilter, 45 | ActionInvoker, 46 | } 47 | 48 | // Do one of each operation. 49 | conf := FilterAction(FakeController.Foo). 50 | Add(NilFilter). 51 | Remove(FlashFilter). 52 | Insert(ValidationFilter, BEFORE, NilFilter). 53 | Insert(I18nFilter, AFTER, NilFilter) 54 | expected := []Filter{ 55 | SessionFilter, 56 | ValidationFilter, 57 | NilFilter, 58 | I18nFilter, 59 | ActionInvoker, 60 | } 61 | actual := getOverride("Foo") 62 | if len(actual) != len(expected) || !filterSliceEqual(actual, expected) { 63 | t.Errorf("Ops failed.\nActual: %#v\nExpect: %#v\nConf:%v", actual, expected, conf) 64 | } 65 | 66 | // Action2 should be unchanged 67 | if getOverride("Bar") != nil { 68 | t.Errorf("Filtering Action should not affect Action2.") 69 | } 70 | 71 | // Test that combining overrides on both the Controller and Action works. 72 | FilterController(FakeController{}). 73 | Add(PanicFilter) 74 | expected = []Filter{ 75 | SessionFilter, 76 | ValidationFilter, 77 | NilFilter, 78 | I18nFilter, 79 | PanicFilter, 80 | ActionInvoker, 81 | } 82 | actual = getOverride("Foo") 83 | if len(actual) != len(expected) || !filterSliceEqual(actual, expected) { 84 | t.Errorf("Expected PanicFilter added to Foo.\nActual: %#v\nExpect: %#v", actual, expected) 85 | } 86 | 87 | expected = []Filter{ 88 | SessionFilter, 89 | FlashFilter, 90 | PanicFilter, 91 | ActionInvoker, 92 | } 93 | actual = getOverride("Bar") 94 | if len(actual) != len(expected) || !filterSliceEqual(actual, expected) { 95 | t.Errorf("Expected PanicFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected) 96 | } 97 | 98 | FilterAction((*FakeController).Bar). 99 | Add(NilFilter) 100 | expected = []Filter{ 101 | SessionFilter, 102 | ValidationFilter, 103 | NilFilter, 104 | I18nFilter, 105 | PanicFilter, 106 | ActionInvoker, 107 | } 108 | actual = getOverride("Foo") 109 | if len(actual) != len(expected) || !filterSliceEqual(actual, expected) { 110 | t.Errorf("Expected no change to Foo.\nActual: %#v\nExpect: %#v", actual, expected) 111 | } 112 | 113 | expected = []Filter{ 114 | SessionFilter, 115 | FlashFilter, 116 | PanicFilter, 117 | NilFilter, 118 | ActionInvoker, 119 | } 120 | actual = getOverride("Bar") 121 | if len(actual) != len(expected) || !filterSliceEqual(actual, expected) { 122 | t.Errorf("Expected NilFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected) 123 | } 124 | } 125 | 126 | func filterSliceEqual(a, e []Filter) bool { 127 | for i, f := range a { 128 | if !FilterEq(f, e[i]) { 129 | return false 130 | } 131 | } 132 | return true 133 | } 134 | 135 | func getOverride(methodName string) []Filter { 136 | return getOverrideChain("FakeController", "FakeController."+methodName) 137 | } 138 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "mime/multipart" 5 | "net/url" 6 | "os" 7 | "reflect" 8 | ) 9 | 10 | // Params provides a unified view of the request params. 11 | // Includes: 12 | // - URL query string 13 | // - Form values 14 | // - File uploads 15 | // 16 | // Warning: param maps other than Values may be nil if there were none. 17 | type Params struct { 18 | url.Values // A unified view of all the individual param maps below. 19 | 20 | // Set by the router 21 | Fixed url.Values // Fixed parameters from the route, e.g. App.Action("fixed param") 22 | Route url.Values // Parameters extracted from the route, e.g. /customers/{id} 23 | 24 | // Set by the ParamsFilter 25 | Query url.Values // Parameters from the query string, e.g. /index?limit=10 26 | Form url.Values // Parameters from the request body. 27 | 28 | Files map[string][]*multipart.FileHeader // Files uploaded in a multipart form 29 | tmpFiles []*os.File // Temp files used during the request. 30 | } 31 | 32 | func ParseParams(params *Params, req *Request) { 33 | params.Query = req.URL.Query() 34 | 35 | // Parse the body depending on the content type. 36 | switch req.ContentType { 37 | case "application/x-www-form-urlencoded": 38 | // Typical form. 39 | if err := req.ParseForm(); err != nil { 40 | WARN.Println("Error parsing request body:", err) 41 | } else { 42 | params.Form = req.Form 43 | } 44 | 45 | case "multipart/form-data": 46 | // Multipart form. 47 | // TODO: Extract the multipart form param so app can set it. 48 | if err := req.ParseMultipartForm(32 << 20 /* 32 MB */); err != nil { 49 | WARN.Println("Error parsing request body:", err) 50 | } else { 51 | params.Form = req.MultipartForm.Value 52 | params.Files = req.MultipartForm.File 53 | } 54 | } 55 | 56 | params.Values = params.calcValues() 57 | } 58 | 59 | // Bind looks for the named parameter, converts it to the requested type, and 60 | // writes it into "dest", which must be settable. If the value can not be 61 | // parsed, "dest" is set to the zero value. 62 | func (p *Params) Bind(dest interface{}, name string) { 63 | value := reflect.ValueOf(dest) 64 | if value.Kind() != reflect.Ptr { 65 | panic("revel/params: non-pointer passed to Bind: " + name) 66 | } 67 | value = value.Elem() 68 | if !value.CanSet() { 69 | panic("revel/params: non-settable variable passed to Bind: " + name) 70 | } 71 | value.Set(Bind(p, name, value.Type())) 72 | } 73 | 74 | // calcValues returns a unified view of the component param maps. 75 | func (p *Params) calcValues() url.Values { 76 | numParams := len(p.Query) + len(p.Fixed) + len(p.Route) + len(p.Form) 77 | 78 | // If there were no params, return an empty map. 79 | if numParams == 0 { 80 | return make(url.Values, 0) 81 | } 82 | 83 | // If only one of the param sources has anything, return that directly. 84 | switch numParams { 85 | case len(p.Query): 86 | return p.Query 87 | case len(p.Route): 88 | return p.Route 89 | case len(p.Fixed): 90 | return p.Fixed 91 | case len(p.Form): 92 | return p.Form 93 | } 94 | 95 | // Copy everything into the same map. 96 | values := make(url.Values, numParams) 97 | for k, v := range p.Fixed { 98 | values[k] = append(values[k], v...) 99 | } 100 | for k, v := range p.Query { 101 | values[k] = append(values[k], v...) 102 | } 103 | for k, v := range p.Route { 104 | values[k] = append(values[k], v...) 105 | } 106 | for k, v := range p.Form { 107 | values[k] = append(values[k], v...) 108 | } 109 | return values 110 | } 111 | 112 | func ParamsFilter(c *Controller, fc []Filter) { 113 | ParseParams(c.Params, c.Request) 114 | 115 | // Clean up from the request. 116 | defer func() { 117 | // Delete temp files. 118 | if c.Request.MultipartForm != nil { 119 | err := c.Request.MultipartForm.RemoveAll() 120 | if err != nil { 121 | WARN.Println("Error removing temporary files:", err) 122 | } 123 | } 124 | 125 | for _, tmpFile := range c.Params.tmpFiles { 126 | err := os.Remove(tmpFile.Name()) 127 | if err != nil { 128 | WARN.Println("Could not remove upload temp file:", err) 129 | } 130 | } 131 | }() 132 | 133 | fc[0](c, fc[1:]) 134 | } 135 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime/debug" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Error description, used as an argument to the error template. 12 | type Error struct { 13 | SourceType string // The type of source that failed to build. 14 | Title, Path, Description string // Description of the error, as presented to the user. 15 | Line, Column int // Where the error was encountered. 16 | SourceLines []string // The entire source file, split into lines. 17 | Stack string // The raw stack trace string from debug.Stack(). 18 | MetaError string // Error that occurred producing the error page. 19 | Link string // A configurable link to wrap the error source in 20 | } 21 | 22 | // An object to hold the per-source-line details. 23 | type sourceLine struct { 24 | Source string 25 | Line int 26 | IsError bool 27 | } 28 | 29 | // NewErrorFromPanic method finds the deepest stack from in user code and 30 | // provide a code listing of that, on the line that eventually triggered 31 | // the panic. Returns nil if no relevant stack frame can be found. 32 | func NewErrorFromPanic(err interface{}) *Error { 33 | 34 | // Parse the filename and line from the originating line of app code. 35 | // /Users/robfig/code/gocode/src/revel/samples/booking/app/controllers/hotels.go:191 (0x44735) 36 | stack := string(debug.Stack()) 37 | frame, basePath := findRelevantStackFrame(stack) 38 | if frame == -1 { 39 | return nil 40 | } 41 | 42 | stack = stack[frame:] 43 | stackElement := stack[:strings.Index(stack, "\n")] 44 | colonIndex := strings.LastIndex(stackElement, ":") 45 | filename := stackElement[:colonIndex] 46 | var line int 47 | fmt.Sscan(stackElement[colonIndex+1:], &line) 48 | 49 | // Show an error page. 50 | description := "Unspecified error" 51 | if err != nil { 52 | description = fmt.Sprint(err) 53 | } 54 | return &Error{ 55 | Title: "Runtime Panic", 56 | Path: filename[len(basePath):], 57 | Line: line, 58 | Description: description, 59 | SourceLines: MustReadLines(filename), 60 | Stack: stack, 61 | } 62 | } 63 | 64 | // Error method constructs a plaintext version of the error, taking 65 | // account that fields are optionally set. Returns e.g. Compilation Error 66 | // (in views/header.html:51): expected right delim in end; got "}" 67 | func (e *Error) Error() string { 68 | loc := "" 69 | if e.Path != "" { 70 | line := "" 71 | if e.Line != 0 { 72 | line = fmt.Sprintf(":%d", e.Line) 73 | } 74 | loc = fmt.Sprintf("(in %s%s)", e.Path, line) 75 | } 76 | header := loc 77 | if e.Title != "" { 78 | if loc != "" { 79 | header = fmt.Sprintf("%s %s: ", e.Title, loc) 80 | } else { 81 | header = fmt.Sprintf("%s: ", e.Title) 82 | } 83 | } 84 | return fmt.Sprintf("%s%s", header, e.Description) 85 | } 86 | 87 | // ContextSource method returns a snippet of the source around 88 | // where the error occurred. 89 | func (e *Error) ContextSource() []sourceLine { 90 | if e.SourceLines == nil { 91 | return nil 92 | } 93 | start := (e.Line - 1) - 5 94 | if start < 0 { 95 | start = 0 96 | } 97 | end := (e.Line - 1) + 5 98 | if end > len(e.SourceLines) { 99 | end = len(e.SourceLines) 100 | } 101 | 102 | var lines []sourceLine = make([]sourceLine, end-start) 103 | for i, src := range e.SourceLines[start:end] { 104 | fileLine := start + i + 1 105 | lines[i] = sourceLine{src, fileLine, fileLine == e.Line} 106 | } 107 | return lines 108 | } 109 | 110 | // SetLink method prepares a link and assign to Error.Link attribute 111 | func (e *Error) SetLink(errorLink string) { 112 | errorLink = strings.Replace(errorLink, "{{Path}}", e.Path, -1) 113 | errorLink = strings.Replace(errorLink, "{{Line}}", strconv.Itoa(e.Line), -1) 114 | 115 | e.Link = "" + e.Path + ":" + strconv.Itoa(e.Line) + "" 116 | } 117 | 118 | // Return the character index of the first relevant stack frame, or -1 if none were found. 119 | // Additionally it returns the base path of the tree in which the identified code resides. 120 | func findRelevantStackFrame(stack string) (int, string) { 121 | if frame := strings.Index(stack, filepath.ToSlash(BasePath)); frame != -1 { 122 | return frame, BasePath 123 | } 124 | for _, module := range Modules { 125 | if frame := strings.Index(stack, filepath.ToSlash(module.Path)); frame != -1 { 126 | return frame, module.Path 127 | } 128 | } 129 | return -1, "" 130 | } 131 | -------------------------------------------------------------------------------- /validators.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "time" 8 | "unicode/utf8" 9 | ) 10 | 11 | type Validator interface { 12 | IsSatisfied(interface{}) bool 13 | DefaultMessage() string 14 | } 15 | 16 | type Required struct{} 17 | 18 | func ValidRequired() Required { 19 | return Required{} 20 | } 21 | 22 | func (r Required) IsSatisfied(obj interface{}) bool { 23 | if obj == nil { 24 | return false 25 | } 26 | 27 | if str, ok := obj.(string); ok { 28 | return utf8.RuneCountInString(str) > 0 29 | } 30 | if b, ok := obj.(bool); ok { 31 | return b 32 | } 33 | if i, ok := obj.(int); ok { 34 | return i != 0 35 | } 36 | if t, ok := obj.(time.Time); ok { 37 | return !t.IsZero() 38 | } 39 | v := reflect.ValueOf(obj) 40 | if v.Kind() == reflect.Slice { 41 | return v.Len() > 0 42 | } 43 | return true 44 | } 45 | 46 | func (r Required) DefaultMessage() string { 47 | return "Required" 48 | } 49 | 50 | type Min struct { 51 | Min int 52 | } 53 | 54 | func ValidMin(min int) Min { 55 | return Min{min} 56 | } 57 | 58 | func (m Min) IsSatisfied(obj interface{}) bool { 59 | num, ok := obj.(int) 60 | if ok { 61 | return num >= m.Min 62 | } 63 | return false 64 | } 65 | 66 | func (m Min) DefaultMessage() string { 67 | return fmt.Sprintln("Minimum is", m.Min) 68 | } 69 | 70 | type Max struct { 71 | Max int 72 | } 73 | 74 | func ValidMax(max int) Max { 75 | return Max{max} 76 | } 77 | 78 | func (m Max) IsSatisfied(obj interface{}) bool { 79 | num, ok := obj.(int) 80 | if ok { 81 | return num <= m.Max 82 | } 83 | return false 84 | } 85 | 86 | func (m Max) DefaultMessage() string { 87 | return fmt.Sprintln("Maximum is", m.Max) 88 | } 89 | 90 | // Requires an integer to be within Min, Max inclusive. 91 | type Range struct { 92 | Min 93 | Max 94 | } 95 | 96 | func ValidRange(min, max int) Range { 97 | return Range{Min{min}, Max{max}} 98 | } 99 | 100 | func (r Range) IsSatisfied(obj interface{}) bool { 101 | return r.Min.IsSatisfied(obj) && r.Max.IsSatisfied(obj) 102 | } 103 | 104 | func (r Range) DefaultMessage() string { 105 | return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max) 106 | } 107 | 108 | // Requires an array or string to be at least a given length. 109 | type MinSize struct { 110 | Min int 111 | } 112 | 113 | func ValidMinSize(min int) MinSize { 114 | return MinSize{min} 115 | } 116 | 117 | func (m MinSize) IsSatisfied(obj interface{}) bool { 118 | if str, ok := obj.(string); ok { 119 | return utf8.RuneCountInString(str) >= m.Min 120 | } 121 | v := reflect.ValueOf(obj) 122 | if v.Kind() == reflect.Slice { 123 | return v.Len() >= m.Min 124 | } 125 | return false 126 | } 127 | 128 | func (m MinSize) DefaultMessage() string { 129 | return fmt.Sprintln("Minimum size is", m.Min) 130 | } 131 | 132 | // Requires an array or string to be at most a given length. 133 | type MaxSize struct { 134 | Max int 135 | } 136 | 137 | func ValidMaxSize(max int) MaxSize { 138 | return MaxSize{max} 139 | } 140 | 141 | func (m MaxSize) IsSatisfied(obj interface{}) bool { 142 | if str, ok := obj.(string); ok { 143 | return utf8.RuneCountInString(str) <= m.Max 144 | } 145 | v := reflect.ValueOf(obj) 146 | if v.Kind() == reflect.Slice { 147 | return v.Len() <= m.Max 148 | } 149 | return false 150 | } 151 | 152 | func (m MaxSize) DefaultMessage() string { 153 | return fmt.Sprintln("Maximum size is", m.Max) 154 | } 155 | 156 | // Requires an array or string to be exactly a given length. 157 | type Length struct { 158 | N int 159 | } 160 | 161 | func ValidLength(n int) Length { 162 | return Length{n} 163 | } 164 | 165 | func (s Length) IsSatisfied(obj interface{}) bool { 166 | if str, ok := obj.(string); ok { 167 | return utf8.RuneCountInString(str) == s.N 168 | } 169 | v := reflect.ValueOf(obj) 170 | if v.Kind() == reflect.Slice { 171 | return v.Len() == s.N 172 | } 173 | return false 174 | } 175 | 176 | func (s Length) DefaultMessage() string { 177 | return fmt.Sprintln("Required length is", s.N) 178 | } 179 | 180 | // Requires a string to match a given regex. 181 | type Match struct { 182 | Regexp *regexp.Regexp 183 | } 184 | 185 | func ValidMatch(regex *regexp.Regexp) Match { 186 | return Match{regex} 187 | } 188 | 189 | func (m Match) IsSatisfied(obj interface{}) bool { 190 | str := obj.(string) 191 | return m.Regexp.MatchString(str) 192 | } 193 | 194 | func (m Match) DefaultMessage() string { 195 | return fmt.Sprintln("Must match", m.Regexp) 196 | } 197 | 198 | var emailPattern = regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$") 199 | 200 | type Email struct { 201 | Match 202 | } 203 | 204 | func ValidEmail() Email { 205 | return Email{Match{emailPattern}} 206 | } 207 | 208 | func (e Email) DefaultMessage() string { 209 | return fmt.Sprintln("Must be a valid email address") 210 | } 211 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "golang.org/x/net/websocket" 12 | ) 13 | 14 | type Request struct { 15 | *http.Request 16 | ContentType string 17 | Format string // "html", "xml", "json", or "txt" 18 | AcceptLanguages AcceptLanguages 19 | Locale string 20 | Websocket *websocket.Conn 21 | } 22 | 23 | type Response struct { 24 | Status int 25 | ContentType string 26 | 27 | Out http.ResponseWriter 28 | } 29 | 30 | func NewResponse(w http.ResponseWriter) *Response { 31 | return &Response{Out: w} 32 | } 33 | 34 | func NewRequest(r *http.Request) *Request { 35 | return &Request{ 36 | Request: r, 37 | ContentType: ResolveContentType(r), 38 | Format: ResolveFormat(r), 39 | AcceptLanguages: ResolveAcceptLanguage(r), 40 | } 41 | } 42 | 43 | // Write the header (for now, just the status code). 44 | // The status may be set directly by the application (c.Response.Status = 501). 45 | // if it isn't, then fall back to the provided status code. 46 | func (resp *Response) WriteHeader(defaultStatusCode int, defaultContentType string) { 47 | if resp.Status == 0 { 48 | resp.Status = defaultStatusCode 49 | } 50 | if resp.ContentType == "" { 51 | resp.ContentType = defaultContentType 52 | } 53 | resp.Out.Header().Set("Content-Type", resp.ContentType) 54 | resp.Out.WriteHeader(resp.Status) 55 | } 56 | 57 | // Get the content type. 58 | // e.g. From "multipart/form-data; boundary=--" to "multipart/form-data" 59 | // If none is specified, returns "text/html" by default. 60 | func ResolveContentType(req *http.Request) string { 61 | contentType := req.Header.Get("Content-Type") 62 | if contentType == "" { 63 | return "text/html" 64 | } 65 | return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0])) 66 | } 67 | 68 | // ResolveFormat maps the request's Accept MIME type declaration to 69 | // a Request.Format attribute, specifically "html", "xml", "json", or "txt", 70 | // returning a default of "html" when Accept header cannot be mapped to a 71 | // value above. 72 | func ResolveFormat(req *http.Request) string { 73 | accept := req.Header.Get("accept") 74 | 75 | switch { 76 | case accept == "", 77 | strings.HasPrefix(accept, "*/*"), // */ 78 | strings.Contains(accept, "application/xhtml"), 79 | strings.Contains(accept, "text/html"): 80 | return "html" 81 | case strings.Contains(accept, "application/json"), 82 | strings.Contains(accept, "text/javascript"), 83 | strings.Contains(accept, "application/javascript"): 84 | return "json" 85 | case strings.Contains(accept, "application/xml"), 86 | strings.Contains(accept, "text/xml"): 87 | return "xml" 88 | case strings.Contains(accept, "text/plain"): 89 | return "txt" 90 | } 91 | 92 | return "html" 93 | } 94 | 95 | // AcceptLanguage is a single language from the Accept-Language HTTP header. 96 | type AcceptLanguage struct { 97 | Language string 98 | Quality float32 99 | } 100 | 101 | // AcceptLanguages is collection of sortable AcceptLanguage instances. 102 | type AcceptLanguages []AcceptLanguage 103 | 104 | func (al AcceptLanguages) Len() int { return len(al) } 105 | func (al AcceptLanguages) Swap(i, j int) { al[i], al[j] = al[j], al[i] } 106 | func (al AcceptLanguages) Less(i, j int) bool { return al[i].Quality > al[j].Quality } 107 | func (al AcceptLanguages) String() string { 108 | output := bytes.NewBufferString("") 109 | for i, language := range al { 110 | output.WriteString(fmt.Sprintf("%s (%1.1f)", language.Language, language.Quality)) 111 | if i != len(al)-1 { 112 | output.WriteString(", ") 113 | } 114 | } 115 | return output.String() 116 | } 117 | 118 | // ResolveAcceptLanguage returns a sorted list of Accept-Language 119 | // header values. 120 | // 121 | // The results are sorted using the quality defined in the header for each 122 | // language range with the most qualified language range as the first 123 | // element in the slice. 124 | // 125 | // See the HTTP header fields specification 126 | // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) for more details. 127 | func ResolveAcceptLanguage(req *http.Request) AcceptLanguages { 128 | header := req.Header.Get("Accept-Language") 129 | if header == "" { 130 | return nil 131 | } 132 | 133 | acceptLanguageHeaderValues := strings.Split(header, ",") 134 | acceptLanguages := make(AcceptLanguages, len(acceptLanguageHeaderValues)) 135 | 136 | for i, languageRange := range acceptLanguageHeaderValues { 137 | if qualifiedRange := strings.Split(languageRange, ";q="); len(qualifiedRange) == 2 { 138 | quality, error := strconv.ParseFloat(qualifiedRange[1], 32) 139 | if error != nil { 140 | WARN.Printf("Detected malformed Accept-Language header quality in '%s', assuming quality is 1", languageRange) 141 | acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], 1} 142 | } else { 143 | acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], float32(quality)} 144 | } 145 | } else { 146 | acceptLanguages[i] = AcceptLanguage{languageRange, 1} 147 | } 148 | } 149 | 150 | sort.Sort(acceptLanguages) 151 | return acceptLanguages 152 | } 153 | -------------------------------------------------------------------------------- /params_test.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | // Params: Testing Multipart forms 14 | 15 | const ( 16 | MULTIPART_BOUNDARY = "A" 17 | MULTIPART_FORM_DATA = `--A 18 | Content-Disposition: form-data; name="text1" 19 | 20 | data1 21 | --A 22 | Content-Disposition: form-data; name="text2" 23 | 24 | data2 25 | --A 26 | Content-Disposition: form-data; name="text2" 27 | 28 | data3 29 | --A 30 | Content-Disposition: form-data; name="file1"; filename="test.txt" 31 | Content-Type: text/plain 32 | 33 | content1 34 | --A 35 | Content-Disposition: form-data; name="file2[]"; filename="test.txt" 36 | Content-Type: text/plain 37 | 38 | content2 39 | --A 40 | Content-Disposition: form-data; name="file2[]"; filename="favicon.ico" 41 | Content-Type: image/x-icon 42 | 43 | xyz 44 | --A 45 | Content-Disposition: form-data; name="file3[0]"; filename="test.txt" 46 | Content-Type: text/plain 47 | 48 | content3 49 | --A 50 | Content-Disposition: form-data; name="file3[1]"; filename="favicon.ico" 51 | Content-Type: image/x-icon 52 | 53 | zzz 54 | --A-- 55 | ` 56 | ) 57 | 58 | // The values represented by the form data. 59 | type fh struct { 60 | filename string 61 | content []byte 62 | } 63 | 64 | var ( 65 | expectedValues = map[string][]string{ 66 | "text1": {"data1"}, 67 | "text2": {"data2", "data3"}, 68 | } 69 | expectedFiles = map[string][]fh{ 70 | "file1": {fh{"test.txt", []byte("content1")}}, 71 | "file2[]": {fh{"test.txt", []byte("content2")}, fh{"favicon.ico", []byte("xyz")}}, 72 | "file3[0]": {fh{"test.txt", []byte("content3")}}, 73 | "file3[1]": {fh{"favicon.ico", []byte("zzz")}}, 74 | } 75 | ) 76 | 77 | func getMultipartRequest() *http.Request { 78 | req, _ := http.NewRequest("POST", "http://localhost/path", 79 | bytes.NewBufferString(MULTIPART_FORM_DATA)) 80 | req.Header.Set( 81 | "Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", MULTIPART_BOUNDARY)) 82 | req.Header.Set( 83 | "Content-Length", fmt.Sprintf("%d", len(MULTIPART_FORM_DATA))) 84 | return req 85 | } 86 | 87 | func BenchmarkParams(b *testing.B) { 88 | c := Controller{ 89 | Request: NewRequest(getMultipartRequest()), 90 | Params: &Params{}, 91 | } 92 | for i := 0; i < b.N; i++ { 93 | ParamsFilter(&c, NilChain) 94 | } 95 | } 96 | 97 | func TestMultipartForm(t *testing.T) { 98 | c := Controller{ 99 | Request: NewRequest(getMultipartRequest()), 100 | Params: &Params{}, 101 | } 102 | ParamsFilter(&c, NilChain) 103 | 104 | if !reflect.DeepEqual(expectedValues, map[string][]string(c.Params.Values)) { 105 | t.Errorf("Param values: (expected) %v != %v (actual)", 106 | expectedValues, map[string][]string(c.Params.Values)) 107 | } 108 | 109 | actualFiles := make(map[string][]fh) 110 | for key, fileHeaders := range c.Params.Files { 111 | for _, fileHeader := range fileHeaders { 112 | file, _ := fileHeader.Open() 113 | content, _ := ioutil.ReadAll(file) 114 | actualFiles[key] = append(actualFiles[key], fh{fileHeader.Filename, content}) 115 | } 116 | } 117 | 118 | if !reflect.DeepEqual(expectedFiles, actualFiles) { 119 | t.Errorf("Param files: (expected) %v != %v (actual)", expectedFiles, actualFiles) 120 | } 121 | } 122 | 123 | func TestBind(t *testing.T) { 124 | params := Params{ 125 | Values: url.Values{ 126 | "x": {"5"}, 127 | }, 128 | } 129 | var x int 130 | params.Bind(&x, "x") 131 | if x != 5 { 132 | t.Errorf("Failed to bind x. Value: %d", x) 133 | } 134 | } 135 | 136 | func TestResolveAcceptLanguage(t *testing.T) { 137 | request := buildHttpRequestWithAcceptLanguage("") 138 | if result := ResolveAcceptLanguage(request); result != nil { 139 | t.Errorf("Expected Accept-Language to resolve to an empty string but it was '%s'", result) 140 | } 141 | 142 | request = buildHttpRequestWithAcceptLanguage("en-GB,en;q=0.8,nl;q=0.6") 143 | if result := ResolveAcceptLanguage(request); len(result) != 3 { 144 | t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3) 145 | } else { 146 | if result[0].Language != "en-GB" { 147 | t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-GB", result[0].Language) 148 | } 149 | if result[1].Language != "en" { 150 | t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en", result[1].Language) 151 | } 152 | if result[2].Language != "nl" { 153 | t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "nl", result[2].Language) 154 | } 155 | } 156 | 157 | request = buildHttpRequestWithAcceptLanguage("en;q=0.8,nl;q=0.6,en-AU;q=malformed") 158 | if result := ResolveAcceptLanguage(request); len(result) != 3 { 159 | t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3) 160 | } else { 161 | if result[0].Language != "en-AU" { 162 | t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-AU", result[0].Language) 163 | } 164 | } 165 | } 166 | 167 | func BenchmarkResolveAcceptLanguage(b *testing.B) { 168 | for i := 0; i < b.N; i++ { 169 | request := buildHttpRequestWithAcceptLanguage("en-GB,en;q=0.8,nl;q=0.6,fr;q=0.5,de-DE;q=0.4,no-NO;q=0.4,ru;q=0.2") 170 | ResolveAcceptLanguage(request) 171 | } 172 | } 173 | 174 | func buildHttpRequestWithAcceptLanguage(acceptLanguage string) *http.Request { 175 | request, _ := http.NewRequest("POST", "http://localhost/path", nil) 176 | request.Header.Set("Accept-Language", acceptLanguage) 177 | return request 178 | } 179 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Length of time to cache an item. 9 | const ( 10 | DEFAULT = time.Duration(0) 11 | FOREVER = time.Duration(-1) 12 | ) 13 | 14 | // Getter is an interface for getting / decoding an element from a cache. 15 | type Getter interface { 16 | // Get the content associated with the given key. decoding it into the given 17 | // pointer. 18 | // 19 | // Returns: 20 | // - nil if the value was successfully retrieved and ptrValue set 21 | // - ErrCacheMiss if the value was not in the cache 22 | // - an implementation specific error otherwise 23 | Get(key string, ptrValue interface{}) error 24 | } 25 | 26 | // Cache is an interface to an expiring cache. It behaves (and is modeled) like 27 | // the Memcached interface. It is keyed by strings (250 bytes at most). 28 | // 29 | // Many callers will make exclusive use of Set and Get, but more exotic 30 | // functions are also available. 31 | // 32 | // Example 33 | // 34 | // Here is a typical Get/Set interaction: 35 | // 36 | // var items []*Item 37 | // if err := cache.Get("items", &items); err != nil { 38 | // items = loadItems() 39 | // go cache.Set("items", items, cache.DEFAULT) 40 | // } 41 | // 42 | // Note that the caller will frequently not wait for Set() to complete. 43 | // 44 | // Errors 45 | // 46 | // It is assumed that callers will infrequently check returned errors, since any 47 | // request should be fulfillable without finding anything in the cache. As a 48 | // result, all errors other than ErrCacheMiss and ErrNotStored will be logged to 49 | // revel.ERROR, so that the developer does not need to check the return value to 50 | // discover things like deserialization or connection errors. 51 | type Cache interface { 52 | // The Cache implements a Getter. 53 | Getter 54 | 55 | // Set the given key/value in the cache, overwriting any existing value 56 | // associated with that key. Keys may be at most 250 bytes in length. 57 | // 58 | // Returns: 59 | // - nil on success 60 | // - an implementation specific error otherwise 61 | Set(key string, value interface{}, expires time.Duration) error 62 | 63 | // Get the content associated multiple keys at once. On success, the caller 64 | // may decode the values one at a time from the returned Getter. 65 | // 66 | // Returns: 67 | // - the value getter, and a nil error if the operation completed. 68 | // - an implementation specific error otherwise 69 | GetMulti(keys ...string) (Getter, error) 70 | 71 | // Delete the given key from the cache. 72 | // 73 | // Returns: 74 | // - nil on a successful delete 75 | // - ErrCacheMiss if the value was not in the cache 76 | // - an implementation specific error otherwise 77 | Delete(key string) error 78 | 79 | // Add the given key/value to the cache ONLY IF the key does not already exist. 80 | // 81 | // Returns: 82 | // - nil if the value was added to the cache 83 | // - ErrNotStored if the key was already present in the cache 84 | // - an implementation-specific error otherwise 85 | Add(key string, value interface{}, expires time.Duration) error 86 | 87 | // Set the given key/value in the cache ONLY IF the key already exists. 88 | // 89 | // Returns: 90 | // - nil if the value was replaced 91 | // - ErrNotStored if the key does not exist in the cache 92 | // - an implementation specific error otherwise 93 | Replace(key string, value interface{}, expires time.Duration) error 94 | 95 | // Increment the value stored at the given key by the given amount. 96 | // The value silently wraps around upon exceeding the uint64 range. 97 | // 98 | // Returns the new counter value if the operation was successful, or: 99 | // - ErrCacheMiss if the key was not found in the cache 100 | // - an implementation specific error otherwise 101 | Increment(key string, n uint64) (newValue uint64, err error) 102 | 103 | // Decrement the value stored at the given key by the given amount. 104 | // The value is capped at 0 on underflow, with no error returned. 105 | // 106 | // Returns the new counter value if the operation was successful, or: 107 | // - ErrCacheMiss if the key was not found in the cache 108 | // - an implementation specific error otherwise 109 | Decrement(key string, n uint64) (newValue uint64, err error) 110 | 111 | // Expire all cache entries immediately. 112 | // This is not implemented for the memcached cache (intentionally). 113 | // Returns an implementation specific error if the operation failed. 114 | Flush() error 115 | } 116 | 117 | var ( 118 | Instance Cache 119 | 120 | ErrCacheMiss = errors.New("revel/cache: key not found.") 121 | ErrNotStored = errors.New("revel/cache: not stored.") 122 | ) 123 | 124 | // The package implements the Cache interface (as sugar). 125 | 126 | func Get(key string, ptrValue interface{}) error { return Instance.Get(key, ptrValue) } 127 | func GetMulti(keys ...string) (Getter, error) { return Instance.GetMulti(keys...) } 128 | func Delete(key string) error { return Instance.Delete(key) } 129 | func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) } 130 | func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) } 131 | func Flush() error { return Instance.Flush() } 132 | func Set(key string, value interface{}, expires time.Duration) error { 133 | return Instance.Set(key, value, expires) 134 | } 135 | func Add(key string, value interface{}, expires time.Duration) error { 136 | return Instance.Add(key, value, expires) 137 | } 138 | func Replace(key string, value interface{}, expires time.Duration) error { 139 | return Instance.Replace(key, value, expires) 140 | } 141 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // A signed cookie (and thus limited to 4kb in size). 15 | // Restriction: Keys may not have a colon in them. 16 | type Session map[string]string 17 | 18 | const ( 19 | SESSION_ID_KEY = "_ID" 20 | TIMESTAMP_KEY = "_TS" 21 | ) 22 | 23 | // expireAfterDuration is the time to live, in seconds, of a session cookie. 24 | // It may be specified in config as "session.expires". Values greater than 0 25 | // set a persistent cookie with a time to live as specified, and the value 0 26 | // sets a session cookie. 27 | var expireAfterDuration time.Duration 28 | 29 | func init() { 30 | // Set expireAfterDuration, default to 30 days if no value in config 31 | OnAppStart(func() { 32 | var err error 33 | if expiresString, ok := Config.String("session.expires"); !ok { 34 | expireAfterDuration = 30 * 24 * time.Hour 35 | } else if expiresString == "session" { 36 | expireAfterDuration = 0 37 | } else if expireAfterDuration, err = time.ParseDuration(expiresString); err != nil { 38 | panic(fmt.Errorf("session.expires invalid: %s", err)) 39 | } 40 | }) 41 | } 42 | 43 | // Id retrieves from the cookie or creates a time-based UUID identifying this 44 | // session. 45 | func (s Session) Id() string { 46 | if sessionIdStr, ok := s[SESSION_ID_KEY]; ok { 47 | return sessionIdStr 48 | } 49 | 50 | buffer := make([]byte, 32) 51 | if _, err := rand.Read(buffer); err != nil { 52 | panic(err) 53 | } 54 | 55 | s[SESSION_ID_KEY] = hex.EncodeToString(buffer) 56 | return s[SESSION_ID_KEY] 57 | } 58 | 59 | // getExpiration return a time.Time with the session's expiration date. 60 | // If previous session has set to "session", remain it 61 | func (s Session) getExpiration() time.Time { 62 | if expireAfterDuration == 0 || s[TIMESTAMP_KEY] == "session" { 63 | // Expire after closing browser 64 | return time.Time{} 65 | } 66 | return time.Now().Add(expireAfterDuration) 67 | } 68 | 69 | // Cookie returns an http.Cookie containing the signed session. 70 | func (s Session) Cookie() *http.Cookie { 71 | var sessionValue string 72 | ts := s.getExpiration() 73 | s[TIMESTAMP_KEY] = getSessionExpirationCookie(ts) 74 | for key, value := range s { 75 | if strings.ContainsAny(key, ":\x00") { 76 | panic("Session keys may not have colons or null bytes") 77 | } 78 | if strings.Contains(value, "\x00") { 79 | panic("Session values may not have null bytes") 80 | } 81 | sessionValue += "\x00" + key + ":" + value + "\x00" 82 | } 83 | 84 | sessionData := url.QueryEscape(sessionValue) 85 | return &http.Cookie{ 86 | Name: CookiePrefix + "_SESSION", 87 | Value: Sign(sessionData) + "-" + sessionData, 88 | Domain: CookieDomain, 89 | Path: "/", 90 | HttpOnly: true, 91 | Secure: CookieSecure, 92 | Expires: ts.UTC(), 93 | } 94 | } 95 | 96 | // sessionTimeoutExpiredOrMissing returns a boolean of whether the session 97 | // cookie is either not present or present but beyond its time to live; i.e., 98 | // whether there is not a valid session. 99 | func sessionTimeoutExpiredOrMissing(session Session) bool { 100 | if exp, present := session[TIMESTAMP_KEY]; !present { 101 | return true 102 | } else if exp == "session" { 103 | return false 104 | } else if expInt, _ := strconv.Atoi(exp); int64(expInt) < time.Now().Unix() { 105 | return true 106 | } 107 | return false 108 | } 109 | 110 | // GetSessionFromCookie returns a Session struct pulled from the signed 111 | // session cookie. 112 | func GetSessionFromCookie(cookie *http.Cookie) Session { 113 | session := make(Session) 114 | 115 | // Separate the data from the signature. 116 | hyphen := strings.Index(cookie.Value, "-") 117 | if hyphen == -1 || hyphen >= len(cookie.Value)-1 { 118 | return session 119 | } 120 | sig, data := cookie.Value[:hyphen], cookie.Value[hyphen+1:] 121 | 122 | // Verify the signature. 123 | if !Verify(data, sig) { 124 | WARN.Println("Session cookie signature failed") 125 | return session 126 | } 127 | 128 | ParseKeyValueCookie(data, func(key, val string) { 129 | session[key] = val 130 | }) 131 | 132 | if sessionTimeoutExpiredOrMissing(session) { 133 | session = make(Session) 134 | } 135 | 136 | return session 137 | } 138 | 139 | // SessionFilter is a Revel Filter that retrieves and sets the session cookie. 140 | // Within Revel, it is available as a Session attribute on Controller instances. 141 | // The name of the Session cookie is set as CookiePrefix + "_SESSION". 142 | func SessionFilter(c *Controller, fc []Filter) { 143 | c.Session = restoreSession(c.Request.Request) 144 | sessionWasEmpty := len(c.Session) == 0 145 | 146 | // Make session vars available in templates as {{.session.xyz}} 147 | c.RenderArgs["session"] = c.Session 148 | 149 | fc[0](c, fc[1:]) 150 | 151 | // Store the signed session if it could have changed. 152 | if len(c.Session) > 0 || !sessionWasEmpty { 153 | c.SetCookie(c.Session.Cookie()) 154 | } 155 | } 156 | 157 | // restoreSession returns either the current session, retrieved from the 158 | // session cookie, or a new session. 159 | func restoreSession(req *http.Request) Session { 160 | cookie, err := req.Cookie(CookiePrefix + "_SESSION") 161 | if err != nil { 162 | return make(Session) 163 | } else { 164 | return GetSessionFromCookie(cookie) 165 | } 166 | } 167 | 168 | // getSessionExpirationCookie retrieves the cookie's time to live as a 169 | // string of either the number of seconds, for a persistent cookie, or 170 | // "session". 171 | func getSessionExpirationCookie(t time.Time) string { 172 | if t.IsZero() { 173 | return "session" 174 | } 175 | return strconv.FormatInt(t.Unix(), 10) 176 | } 177 | 178 | // SetNoExpiration sets session to expire when browser session ends 179 | func (s Session) SetNoExpiration() { 180 | s[TIMESTAMP_KEY] = "session" 181 | } 182 | 183 | // SetDefaultExpiration sets session to expire after default duration 184 | func (s Session) SetDefaultExpiration() { 185 | delete(s, TIMESTAMP_KEY) 186 | } 187 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | "sync" 9 | 10 | "gopkg.in/fsnotify.v1" 11 | ) 12 | 13 | // Listener is an interface for receivers of filesystem events. 14 | type Listener interface { 15 | // Refresh is invoked by the watcher on relevant filesystem events. 16 | // If the listener returns an error, it is served to the user on the current request. 17 | Refresh() *Error 18 | } 19 | 20 | // DiscerningListener allows the receiver to selectively watch files. 21 | type DiscerningListener interface { 22 | Listener 23 | WatchDir(info os.FileInfo) bool 24 | WatchFile(basename string) bool 25 | } 26 | 27 | // Watcher allows listeners to register to be notified of changes under a given 28 | // directory. 29 | type Watcher struct { 30 | // Parallel arrays of watcher/listener pairs. 31 | watchers []*fsnotify.Watcher 32 | listeners []Listener 33 | forceRefresh bool 34 | lastError int 35 | notifyMutex sync.Mutex 36 | } 37 | 38 | func NewWatcher() *Watcher { 39 | return &Watcher{ 40 | forceRefresh: true, 41 | lastError: -1, 42 | } 43 | } 44 | 45 | // Listen registers for events within the given root directories (recursively). 46 | func (w *Watcher) Listen(listener Listener, roots ...string) { 47 | watcher, err := fsnotify.NewWatcher() 48 | if err != nil { 49 | ERROR.Fatal(err) 50 | } 51 | 52 | // Replace the unbuffered Event channel with a buffered one. 53 | // Otherwise multiple change events only come out one at a time, across 54 | // multiple page views. (There appears no way to "pump" the events out of 55 | // the watcher) 56 | watcher.Events = make(chan fsnotify.Event, 100) 57 | watcher.Errors = make(chan error, 10) 58 | 59 | // Walk through all files / directories under the root, adding each to watcher. 60 | for _, p := range roots { 61 | // is the directory / file a symlink? 62 | f, err := os.Lstat(p) 63 | if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink { 64 | realPath, err := filepath.EvalSymlinks(p) 65 | if err != nil { 66 | panic(err) 67 | } 68 | p = realPath 69 | } 70 | 71 | fi, err := os.Stat(p) 72 | if err != nil { 73 | ERROR.Println("Failed to stat watched path", p, ":", err) 74 | continue 75 | } 76 | 77 | // If it is a file, watch that specific file. 78 | if !fi.IsDir() { 79 | err = watcher.Add(p) 80 | if err != nil { 81 | ERROR.Println("Failed to watch", p, ":", err) 82 | } 83 | continue 84 | } 85 | 86 | var watcherWalker func(path string, info os.FileInfo, err error) error 87 | 88 | watcherWalker = func(path string, info os.FileInfo, err error) error { 89 | if err != nil { 90 | ERROR.Println("Error walking path:", err) 91 | return nil 92 | } 93 | 94 | if info.IsDir() { 95 | if dl, ok := listener.(DiscerningListener); ok { 96 | if !dl.WatchDir(info) { 97 | return filepath.SkipDir 98 | } 99 | } 100 | 101 | err = watcher.Add(path) 102 | if err != nil { 103 | ERROR.Println("Failed to watch", path, ":", err) 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | // Else, walk the directory tree. 110 | err = Walk(p, watcherWalker) 111 | if err != nil { 112 | ERROR.Println("Failed to walk directory", p, ":", err) 113 | } 114 | } 115 | 116 | if w.eagerRebuildEnabled() { 117 | // Create goroutine to notify file changes in real time 118 | go w.NotifyWhenUpdated(listener, watcher) 119 | } 120 | 121 | w.watchers = append(w.watchers, watcher) 122 | w.listeners = append(w.listeners, listener) 123 | } 124 | 125 | // NotifyWhenUpdated notifies the watcher when a file event is received. 126 | func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) { 127 | for { 128 | select { 129 | case ev := <-watcher.Events: 130 | if w.rebuildRequired(ev, listener) { 131 | // Serialize listener.Refresh() calls. 132 | w.notifyMutex.Lock() 133 | listener.Refresh() 134 | w.notifyMutex.Unlock() 135 | } 136 | case <-watcher.Errors: 137 | continue 138 | } 139 | } 140 | } 141 | 142 | // Notify causes the watcher to forward any change events to listeners. 143 | // It returns the first (if any) error returned. 144 | func (w *Watcher) Notify() *Error { 145 | // Serialize Notify() calls. 146 | w.notifyMutex.Lock() 147 | defer w.notifyMutex.Unlock() 148 | 149 | for i, watcher := range w.watchers { 150 | listener := w.listeners[i] 151 | 152 | // Pull all pending events / errors from the watcher. 153 | refresh := false 154 | for { 155 | select { 156 | case ev := <-watcher.Events: 157 | if w.rebuildRequired(ev, listener) { 158 | refresh = true 159 | } 160 | continue 161 | case <-watcher.Errors: 162 | continue 163 | default: 164 | // No events left to pull 165 | } 166 | break 167 | } 168 | 169 | if w.forceRefresh || refresh || w.lastError == i { 170 | err := listener.Refresh() 171 | if err != nil { 172 | w.lastError = i 173 | return err 174 | } 175 | } 176 | } 177 | 178 | w.forceRefresh = false 179 | w.lastError = -1 180 | return nil 181 | } 182 | 183 | // If watch.mode is set to eager, the application is rebuilt immediately 184 | // when a source file is changed. 185 | // This feature is available only in dev mode. 186 | func (w *Watcher) eagerRebuildEnabled() bool { 187 | return Config.BoolDefault("mode.dev", true) && 188 | Config.BoolDefault("watch", true) && 189 | Config.StringDefault("watch.mode", "normal") == "eager" 190 | } 191 | 192 | func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool { 193 | // Ignore changes to dotfiles. 194 | if strings.HasPrefix(path.Base(ev.Name), ".") { 195 | return false 196 | } 197 | 198 | if dl, ok := listener.(DiscerningListener); ok { 199 | if !dl.WatchFile(ev.Name) || ev.Op&fsnotify.Chmod == fsnotify.Chmod { 200 | return false 201 | } 202 | } 203 | return true 204 | } 205 | 206 | var WatchFilter = func(c *Controller, fc []Filter) { 207 | if MainWatcher != nil { 208 | err := MainWatcher.Notify() 209 | if err != nil { 210 | c.Result = c.RenderError(err) 211 | return 212 | } 213 | } 214 | fc[0](c, fc[1:]) 215 | } 216 | -------------------------------------------------------------------------------- /compress.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/klauspost/compress/gzip" 10 | "github.com/klauspost/compress/zlib" 11 | ) 12 | 13 | var compressionTypes = [...]string{ 14 | "gzip", 15 | "deflate", 16 | } 17 | 18 | var compressableMimes = [...]string{ 19 | "text/plain", 20 | "text/html", 21 | "text/xml", 22 | "text/css", 23 | "application/json", 24 | "application/xml", 25 | "application/xhtml+xml", 26 | "application/rss+xml", 27 | "application/javascript", 28 | "application/x-javascript", 29 | } 30 | 31 | type WriteFlusher interface { 32 | io.Writer 33 | io.Closer 34 | Flush() error 35 | } 36 | 37 | type CompressResponseWriter struct { 38 | http.ResponseWriter 39 | compressWriter WriteFlusher 40 | compressionType string 41 | headersWritten bool 42 | closeNotify chan bool 43 | parentNotify <-chan bool 44 | closed bool 45 | } 46 | 47 | func CompressFilter(c *Controller, fc []Filter) { 48 | fc[0](c, fc[1:]) 49 | if Config.BoolDefault("results.compressed", false) { 50 | if c.Response.Status != http.StatusNoContent && c.Response.Status != http.StatusNotModified { 51 | writer := CompressResponseWriter{c.Response.Out, nil, "", false, make(chan bool, 1), nil, false} 52 | writer.DetectCompressionType(c.Request, c.Response) 53 | w, ok := c.Response.Out.(http.CloseNotifier) 54 | if ok { 55 | writer.parentNotify = w.CloseNotify() 56 | } 57 | c.Response.Out = &writer 58 | } else { 59 | TRACE.Printf("Compression disabled for response status (%d)", c.Response.Status) 60 | } 61 | } 62 | } 63 | 64 | func (c CompressResponseWriter) CloseNotify() <-chan bool { 65 | if c.parentNotify != nil { 66 | return c.parentNotify 67 | } 68 | return c.closeNotify 69 | } 70 | 71 | func (c *CompressResponseWriter) prepareHeaders() { 72 | if c.compressionType != "" { 73 | responseMime := c.Header().Get("Content-Type") 74 | responseMime = strings.TrimSpace(strings.SplitN(responseMime, ";", 2)[0]) 75 | shouldEncode := false 76 | 77 | if c.Header().Get("Content-Encoding") == "" { 78 | for _, compressableMime := range compressableMimes { 79 | if responseMime == compressableMime { 80 | shouldEncode = true 81 | c.Header().Set("Content-Encoding", c.compressionType) 82 | c.Header().Del("Content-Length") 83 | break 84 | } 85 | } 86 | } 87 | 88 | if !shouldEncode { 89 | c.compressWriter = nil 90 | c.compressionType = "" 91 | } 92 | } 93 | } 94 | 95 | func (c *CompressResponseWriter) WriteHeader(status int) { 96 | c.headersWritten = true 97 | c.prepareHeaders() 98 | c.ResponseWriter.WriteHeader(status) 99 | } 100 | 101 | func (c *CompressResponseWriter) Close() error { 102 | if c.compressionType != "" { 103 | _ = c.compressWriter.Close() 104 | } 105 | if w, ok := c.ResponseWriter.(io.Closer); ok { 106 | _ = w.Close() 107 | } 108 | // Non-blocking write to the closenotifier, if we for some reason should 109 | // get called multiple times 110 | select { 111 | case c.closeNotify <- true: 112 | default: 113 | } 114 | c.closed = true 115 | return nil 116 | } 117 | 118 | func (c *CompressResponseWriter) Write(b []byte) (int, error) { 119 | // Abort if parent has been closed 120 | if c.parentNotify != nil { 121 | select { 122 | case <-c.parentNotify: 123 | return 0, io.ErrClosedPipe 124 | default: 125 | } 126 | } 127 | // Abort if we ourselves have been closed 128 | if c.closed { 129 | return 0, io.ErrClosedPipe 130 | } 131 | if !c.headersWritten { 132 | c.prepareHeaders() 133 | c.headersWritten = true 134 | } 135 | 136 | if c.compressionType != "" { 137 | return c.compressWriter.Write(b) 138 | } 139 | 140 | return c.ResponseWriter.Write(b) 141 | } 142 | 143 | // DetectCompressionType method detects the comperssion type 144 | // from header "Accept-Encoding" 145 | func (c *CompressResponseWriter) DetectCompressionType(req *Request, resp *Response) { 146 | if Config.BoolDefault("results.compressed", false) { 147 | acceptedEncodings := strings.Split(req.Request.Header.Get("Accept-Encoding"), ",") 148 | 149 | largestQ := 0.0 150 | chosenEncoding := len(compressionTypes) 151 | 152 | // I have fixed one edge case for issue #914 153 | // But it's better to cover all possible edge cases or 154 | // Adapt to https://github.com/golang/gddo/blob/master/httputil/header/header.go#L172 155 | for _, encoding := range acceptedEncodings { 156 | encoding = strings.TrimSpace(encoding) 157 | encodingParts := strings.SplitN(encoding, ";", 2) 158 | 159 | // If we are the format "gzip;q=0.8" 160 | if len(encodingParts) > 1 { 161 | q := strings.TrimSpace(encodingParts[1]) 162 | if len(q) == 0 || !strings.HasPrefix(q, "q=") { 163 | continue 164 | } 165 | 166 | // Strip off the q= 167 | num, err := strconv.ParseFloat(q[2:], 32) 168 | if err != nil { 169 | continue 170 | } 171 | 172 | if num >= largestQ && num > 0 { 173 | if encodingParts[0] == "*" { 174 | chosenEncoding = 0 175 | largestQ = num 176 | continue 177 | } 178 | for i, encoding := range compressionTypes { 179 | if encoding == encodingParts[0] { 180 | if i < chosenEncoding { 181 | largestQ = num 182 | chosenEncoding = i 183 | } 184 | break 185 | } 186 | } 187 | } 188 | } else { 189 | // If we can accept anything, chose our preferred method. 190 | if encodingParts[0] == "*" { 191 | chosenEncoding = 0 192 | largestQ = 1 193 | break 194 | } 195 | // This is for just plain "gzip" 196 | for i, encoding := range compressionTypes { 197 | if encoding == encodingParts[0] { 198 | if i < chosenEncoding { 199 | largestQ = 1.0 200 | chosenEncoding = i 201 | } 202 | break 203 | } 204 | } 205 | } 206 | } 207 | 208 | if largestQ == 0 { 209 | return 210 | } 211 | 212 | c.compressionType = compressionTypes[chosenEncoding] 213 | 214 | switch c.compressionType { 215 | case "gzip": 216 | c.compressWriter = gzip.NewWriter(resp.Out) 217 | case "deflate": 218 | c.compressWriter = zlib.NewWriter(resp.Out) 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /intercept.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | ) 7 | 8 | // An "interceptor" is functionality invoked by the framework BEFORE or AFTER 9 | // an action. 10 | // 11 | // An interceptor may optionally return a Result (instead of nil). Depending on 12 | // when the interceptor was invoked, the response is different: 13 | // 1. BEFORE: No further interceptors are invoked, and neither is the action. 14 | // 2. AFTER: Further interceptors are still run. 15 | // In all cases, any returned Result will take the place of any existing Result. 16 | // 17 | // In the BEFORE case, that returned Result is guaranteed to be final, while 18 | // in the AFTER case it is possible that a further interceptor could emit its 19 | // own Result. 20 | // 21 | // Interceptors are called in the order that they are added. 22 | // 23 | // *** 24 | // 25 | // Two types of interceptors are provided: Funcs and Methods 26 | // 27 | // Func Interceptors may apply to any / all Controllers. 28 | // 29 | // func example(*revel.Controller) revel.Result 30 | // 31 | // Method Interceptors are provided so that properties can be set on application 32 | // controllers. 33 | // 34 | // func (c AppController) example() revel.Result 35 | // func (c *AppController) example() revel.Result 36 | // 37 | type InterceptorFunc func(*Controller) Result 38 | type InterceptorMethod interface{} 39 | type When int 40 | 41 | const ( 42 | BEFORE When = iota 43 | AFTER 44 | PANIC 45 | FINALLY 46 | ) 47 | 48 | type InterceptTarget int 49 | 50 | const ( 51 | ALL_CONTROLLERS InterceptTarget = iota 52 | ) 53 | 54 | type Interception struct { 55 | When When 56 | 57 | function InterceptorFunc 58 | method InterceptorMethod 59 | 60 | callable reflect.Value 61 | target reflect.Type 62 | interceptAll bool 63 | } 64 | 65 | // Perform the given interception. 66 | // val is a pointer to the App Controller. 67 | func (i Interception) Invoke(val reflect.Value) reflect.Value { 68 | var arg reflect.Value 69 | if i.function == nil { 70 | // If it's an InterceptorMethod, then we have to pass in the target type. 71 | arg = findTarget(val, i.target) 72 | } else { 73 | // If it's an InterceptorFunc, then the type must be *Controller. 74 | // We can find that by following the embedded types up the chain. 75 | for val.Type() != controllerPtrType { 76 | if val.Kind() == reflect.Ptr { 77 | val = val.Elem() 78 | } 79 | val = val.Field(0) 80 | } 81 | arg = val 82 | } 83 | 84 | vals := i.callable.Call([]reflect.Value{arg}) 85 | return vals[0] 86 | } 87 | 88 | func InterceptorFilter(c *Controller, fc []Filter) { 89 | defer invokeInterceptors(FINALLY, c) 90 | defer func() { 91 | if err := recover(); err != nil { 92 | invokeInterceptors(PANIC, c) 93 | panic(err) 94 | } 95 | }() 96 | 97 | // Invoke the BEFORE interceptors and return early, if we get a result. 98 | invokeInterceptors(BEFORE, c) 99 | if c.Result != nil { 100 | return 101 | } 102 | 103 | fc[0](c, fc[1:]) 104 | invokeInterceptors(AFTER, c) 105 | } 106 | 107 | func invokeInterceptors(when When, c *Controller) { 108 | var ( 109 | app = reflect.ValueOf(c.AppController) 110 | result Result 111 | ) 112 | for _, intc := range getInterceptors(when, app) { 113 | resultValue := intc.Invoke(app) 114 | if !resultValue.IsNil() { 115 | result = resultValue.Interface().(Result) 116 | } 117 | if when == BEFORE && result != nil { 118 | c.Result = result 119 | return 120 | } 121 | } 122 | if result != nil { 123 | c.Result = result 124 | } 125 | } 126 | 127 | var interceptors []*Interception 128 | 129 | // Install a general interceptor. 130 | // This can be applied to any Controller. 131 | // It must have the signature of: 132 | // func example(c *revel.Controller) revel.Result 133 | func InterceptFunc(intc InterceptorFunc, when When, target interface{}) { 134 | interceptors = append(interceptors, &Interception{ 135 | When: when, 136 | function: intc, 137 | callable: reflect.ValueOf(intc), 138 | target: reflect.TypeOf(target), 139 | interceptAll: target == ALL_CONTROLLERS, 140 | }) 141 | } 142 | 143 | // Install an interceptor method that applies to its own Controller. 144 | // func (c AppController) example() revel.Result 145 | // func (c *AppController) example() revel.Result 146 | func InterceptMethod(intc InterceptorMethod, when When) { 147 | methodType := reflect.TypeOf(intc) 148 | if methodType.Kind() != reflect.Func || methodType.NumOut() != 1 || methodType.NumIn() != 1 { 149 | log.Fatalln("Interceptor method should have signature like", 150 | "'func (c *AppController) example() revel.Result' but was", methodType) 151 | } 152 | interceptors = append(interceptors, &Interception{ 153 | When: when, 154 | method: intc, 155 | callable: reflect.ValueOf(intc), 156 | target: methodType.In(0), 157 | }) 158 | } 159 | 160 | func getInterceptors(when When, val reflect.Value) []*Interception { 161 | result := []*Interception{} 162 | for _, intc := range interceptors { 163 | if intc.When != when { 164 | continue 165 | } 166 | 167 | if intc.interceptAll || findTarget(val, intc.target).IsValid() { 168 | result = append(result, intc) 169 | } 170 | } 171 | return result 172 | } 173 | 174 | // Find the value of the target, starting from val and including embedded types. 175 | // Also, convert between any difference in indirection. 176 | // If the target couldn't be found, the returned Value will have IsValid() == false 177 | func findTarget(val reflect.Value, target reflect.Type) reflect.Value { 178 | // Look through the embedded types (until we reach the *revel.Controller at the top). 179 | valueQueue := []reflect.Value{val} 180 | for len(valueQueue) > 0 { 181 | val, valueQueue = valueQueue[0], valueQueue[1:] 182 | 183 | // Check if val is of a similar type to the target type. 184 | if val.Type() == target { 185 | return val 186 | } 187 | if val.Kind() == reflect.Ptr && val.Elem().Type() == target { 188 | return val.Elem() 189 | } 190 | if target.Kind() == reflect.Ptr && target.Elem() == val.Type() { 191 | return val.Addr() 192 | } 193 | 194 | // If we reached the *revel.Controller and still didn't find what we were 195 | // looking for, give up. 196 | if val.Type() == controllerPtrType { 197 | continue 198 | } 199 | 200 | // Else, add each anonymous field to the queue. 201 | if val.Kind() == reflect.Ptr { 202 | val = val.Elem() 203 | } 204 | 205 | for i := 0; i < val.NumField(); i++ { 206 | if val.Type().Field(i).Anonymous { 207 | valueQueue = append(valueQueue, val.Field(i)) 208 | } 209 | } 210 | } 211 | 212 | return reflect.Value{} 213 | } 214 | -------------------------------------------------------------------------------- /skeleton/conf/app.conf.template: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Revel configuration file 3 | # See: 4 | # http://revel.github.io/manual/appconf.html 5 | # for more detailed documentation. 6 | ################################################################################ 7 | 8 | # This sets the `AppName` variable which can be used in your code as 9 | # `if revel.AppName {...}` 10 | app.name = {{ .AppName }} 11 | 12 | # A secret string which is passed to cryptographically sign the cookie to prevent 13 | # (and detect) user modification. 14 | # Keep this string secret or users will be able to inject arbitrary cookie values 15 | # into your application 16 | app.secret = {{ .Secret }} 17 | 18 | # Revel running behind proxy like nginx, haproxy, etc 19 | app.behind.proxy = false 20 | 21 | 22 | # The IP address on which to listen. 23 | http.addr = 24 | 25 | # The port on which to listen. 26 | http.port = 9000 27 | 28 | # Whether to use SSL or not. 29 | http.ssl = false 30 | 31 | # Path to an X509 certificate file, if using SSL. 32 | #http.sslcert = 33 | 34 | # Path to an X509 certificate key, if using SSL. 35 | #http.sslkey = 36 | 37 | 38 | # For any cookies set by Revel (Session,Flash,Error) these properties will set 39 | # the fields of: 40 | # http://golang.org/pkg/net/http/#Cookie 41 | # 42 | # Each cookie set by Revel is prefixed with this string. 43 | cookie.prefix = REVEL 44 | 45 | # A secure cookie has the secure attribute enabled and is only used via HTTPS, 46 | # ensuring that the cookie is always encrypted when transmitting from client to 47 | # server. This makes the cookie less likely to be exposed to cookie theft via 48 | # eavesdropping. 49 | # 50 | # In dev mode, this will default to false, otherwise it will 51 | # default to true. 52 | # cookie.secure = false 53 | 54 | # Limit cookie access to a given domain 55 | #cookie.domain = 56 | 57 | # Define when your session cookie expires. Possible values: 58 | # "720h" 59 | # A time duration (http://golang.org/pkg/time/#ParseDuration) after which 60 | # the cookie expires and the session is invalid. 61 | # "session" 62 | # Sets a session cookie which invalidates the session when the user close 63 | # the browser. 64 | session.expires = 720h 65 | 66 | 67 | # The date format used by Revel. Possible formats defined by the Go `time` 68 | # package (http://golang.org/pkg/time/#Parse) 69 | format.date = 2006-01-02 70 | format.datetime = 2006-01-02 15:04 71 | 72 | # Timeout specifies a time limit for request (in seconds) made by a single client. 73 | # A Timeout of zero means no timeout. 74 | timeout.read = 90 75 | timeout.write = 60 76 | 77 | 78 | # Determines whether the template rendering should use chunked encoding. 79 | # Chunked encoding can decrease the time to first byte on the client side by 80 | # sending data before the entire template has been fully rendered. 81 | results.chunked = false 82 | 83 | 84 | # Prefixes for each log message line 85 | # User can override these prefix values within any section 86 | # For e.g: [dev], [prod], etc 87 | log.trace.prefix = "TRACE " 88 | log.info.prefix = "INFO " 89 | log.warn.prefix = "WARN " 90 | log.error.prefix = "ERROR " 91 | 92 | 93 | # The default language of this application. 94 | i18n.default_language = en 95 | 96 | # The default format when message is missing. 97 | # The original message shows in %s 98 | #i18n.unknown_format = "??? %s ???" 99 | 100 | 101 | # Module to serve static content such as CSS, JavaScript and Media files 102 | # Allows Routes like this: 103 | # `Static.ServeModule("modulename","public")` 104 | module.static=github.com/revel/modules/static 105 | 106 | 107 | 108 | ################################################################################ 109 | # Section: dev 110 | # This section is evaluated when running Revel in dev mode. Like so: 111 | # `revel run path/to/myapp` 112 | [dev] 113 | # This sets `DevMode` variable to `true` which can be used in your code as 114 | # `if revel.DevMode {...}` 115 | # or in your templates with 116 | # `{{.DevMode}}` 117 | mode.dev = true 118 | 119 | 120 | # Pretty print JSON/XML when calling RenderJson/RenderXml 121 | results.pretty = true 122 | 123 | 124 | # Automatically watches your applicaton files and recompiles on-demand 125 | watch = true 126 | 127 | 128 | # If you set watch.mode = "eager", the server starts to recompile 129 | # your application every time your application's files change. 130 | watch.mode = "normal" 131 | 132 | # Watch the entire $GOPATH for code changes. Default is false. 133 | #watch.gopath = true 134 | 135 | 136 | # Module to run code tests in the browser 137 | # See: 138 | # http://revel.github.io/manual/testing.html 139 | module.testrunner = github.com/revel/modules/testrunner 140 | 141 | 142 | # Where to log the various Revel logs 143 | log.trace.output = off 144 | log.info.output = stderr 145 | log.warn.output = stderr 146 | log.error.output = stderr 147 | 148 | 149 | # Revel log flags. Possible flags defined by the Go `log` package, 150 | # please refer https://golang.org/pkg/log/#pkg-constants 151 | # Go log is "Bits or'ed together to control what's printed" 152 | # Examples: 153 | # 0 => just log the message, turn off the flags 154 | # 3 => log.LstdFlags (log.Ldate|log.Ltime) 155 | # 19 => log.Ldate|log.Ltime|log.Lshortfile 156 | # 23 => log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile 157 | log.trace.flags = 19 158 | log.info.flags = 19 159 | log.warn.flags = 19 160 | log.error.flags = 19 161 | 162 | 163 | # Revel request access log 164 | # Access log line format: 165 | # RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath 166 | # Sample format: 167 | # 2016/05/25 17:46:37.112 127.0.0.1 200 270.157µs GET / 168 | log.request.output = stderr 169 | 170 | 171 | ################################################################################ 172 | # Section: prod 173 | # This section is evaluated when running Revel in production mode. Like so: 174 | # `revel run path/to/myapp prod` 175 | # See: 176 | # [dev] section for documentation of the various settings 177 | [prod] 178 | mode.dev = false 179 | 180 | 181 | results.pretty = false 182 | 183 | 184 | watch = false 185 | 186 | 187 | module.testrunner = 188 | 189 | 190 | log.trace.output = off 191 | log.info.output = off 192 | log.warn.output = log/%(app.name)s.log 193 | log.error.output = log/%(app.name)s.log 194 | 195 | # Revel log flags. Possible flags defined by the Go `log` package, 196 | # please refer https://golang.org/pkg/log/#pkg-constants 197 | # Go log is "Bits or'ed together to control what's printed" 198 | # Examples: 199 | # 0 => just log the message, turn off the flags 200 | # 3 => log.LstdFlags (log.Ldate|log.Ltime) 201 | # 19 => log.Ldate|log.Ltime|log.Lshortfile 202 | # 23 => log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile 203 | log.trace.flags = 3 204 | log.info.flags = 3 205 | log.warn.flags = 3 206 | log.error.flags = 3 207 | 208 | 209 | # Revel request access log 210 | # Access log line format: 211 | # RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath 212 | # Sample format: 213 | # 2016/05/25 17:46:37.112 127.0.0.1 200 270.157µs GET / 214 | # Example: 215 | # log.request.output = %(app.name)s-request.log 216 | log.request.output = off 217 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // Tests against a generic Cache interface. 10 | // They should pass for all implementations. 11 | type cacheFactory func(*testing.T, time.Duration) Cache 12 | 13 | // Test typical cache interactions 14 | func typicalGetSet(t *testing.T, newCache cacheFactory) { 15 | var err error 16 | cache := newCache(t, time.Hour) 17 | 18 | value := "foo" 19 | if err = cache.Set("value", value, DEFAULT); err != nil { 20 | t.Errorf("Error setting a value: %s", err) 21 | } 22 | 23 | value = "" 24 | err = cache.Get("value", &value) 25 | if err != nil { 26 | t.Errorf("Error getting a value: %s", err) 27 | } 28 | if value != "foo" { 29 | t.Errorf("Expected to get foo back, got %s", value) 30 | } 31 | } 32 | 33 | // Test the increment-decrement cases 34 | func incrDecr(t *testing.T, newCache cacheFactory) { 35 | var err error 36 | cache := newCache(t, time.Hour) 37 | 38 | // Normal increment / decrement operation. 39 | if err = cache.Set("int", 10, DEFAULT); err != nil { 40 | t.Errorf("Error setting int: %s", err) 41 | } 42 | newValue, err := cache.Increment("int", 50) 43 | if err != nil { 44 | t.Errorf("Error incrementing int: %s", err) 45 | } 46 | if newValue != 60 { 47 | t.Errorf("Expected 60, was %d", newValue) 48 | } 49 | 50 | if newValue, err = cache.Decrement("int", 50); err != nil { 51 | t.Errorf("Error decrementing: %s", err) 52 | } 53 | if newValue != 10 { 54 | t.Errorf("Expected 10, was %d", newValue) 55 | } 56 | 57 | // Increment wraparound 58 | newValue, err = cache.Increment("int", math.MaxUint64-5) 59 | if err != nil { 60 | t.Errorf("Error wrapping around: %s", err) 61 | } 62 | if newValue != 4 { 63 | t.Errorf("Expected wraparound 4, got %d", newValue) 64 | } 65 | 66 | // Decrement capped at 0 67 | newValue, err = cache.Decrement("int", 25) 68 | if err != nil { 69 | t.Errorf("Error decrementing below 0: %s", err) 70 | } 71 | if newValue != 0 { 72 | t.Errorf("Expected capped at 0, got %d", newValue) 73 | } 74 | } 75 | 76 | func expiration(t *testing.T, newCache cacheFactory) { 77 | // memcached does not support expiration times less than 1 second. 78 | var err error 79 | cache := newCache(t, time.Second) 80 | // Test Set w/ DEFAULT 81 | value := 10 82 | cache.Set("int", value, DEFAULT) 83 | time.Sleep(2 * time.Second) 84 | err = cache.Get("int", &value) 85 | if err != ErrCacheMiss { 86 | t.Errorf("Expected CacheMiss, but got: %s", err) 87 | } 88 | 89 | // Test Set w/ short time 90 | cache.Set("int", value, time.Second) 91 | time.Sleep(2 * time.Second) 92 | err = cache.Get("int", &value) 93 | if err != ErrCacheMiss { 94 | t.Errorf("Expected CacheMiss, but got: %s", err) 95 | } 96 | 97 | // Test Set w/ longer time. 98 | cache.Set("int", value, time.Hour) 99 | time.Sleep(2 * time.Second) 100 | err = cache.Get("int", &value) 101 | if err != nil { 102 | t.Errorf("Expected to get the value, but got: %s", err) 103 | } 104 | 105 | // Test Set w/ forever. 106 | cache.Set("int", value, FOREVER) 107 | time.Sleep(2 * time.Second) 108 | err = cache.Get("int", &value) 109 | if err != nil { 110 | t.Errorf("Expected to get the value, but got: %s", err) 111 | } 112 | } 113 | 114 | func emptyCache(t *testing.T, newCache cacheFactory) { 115 | var err error 116 | cache := newCache(t, time.Hour) 117 | 118 | err = cache.Get("notexist", 0) 119 | if err == nil { 120 | t.Errorf("Error expected for non-existent key") 121 | } 122 | if err != ErrCacheMiss { 123 | t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err) 124 | } 125 | 126 | err = cache.Delete("notexist") 127 | if err != ErrCacheMiss { 128 | t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err) 129 | } 130 | 131 | _, err = cache.Increment("notexist", 1) 132 | if err != ErrCacheMiss { 133 | t.Errorf("Expected cache miss incrementing non-existent key: %s", err) 134 | } 135 | 136 | _, err = cache.Decrement("notexist", 1) 137 | if err != ErrCacheMiss { 138 | t.Errorf("Expected cache miss decrementing non-existent key: %s", err) 139 | } 140 | } 141 | 142 | func testReplace(t *testing.T, newCache cacheFactory) { 143 | var err error 144 | cache := newCache(t, time.Hour) 145 | 146 | // Replace in an empty cache. 147 | if err = cache.Replace("notexist", 1, FOREVER); err != ErrNotStored { 148 | t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err) 149 | } 150 | 151 | // Set a value of 1, and replace it with 2 152 | if err = cache.Set("int", 1, time.Second); err != nil { 153 | t.Errorf("Unexpected error: %s", err) 154 | } 155 | 156 | if err = cache.Replace("int", 2, time.Second); err != nil { 157 | t.Errorf("Unexpected error: %s", err) 158 | } 159 | var i int 160 | if err = cache.Get("int", &i); err != nil { 161 | t.Errorf("Unexpected error getting a replaced item: %s", err) 162 | } 163 | if i != 2 { 164 | t.Errorf("Expected 2, got %d", i) 165 | } 166 | 167 | // Wait for it to expire and replace with 3 (unsuccessfully). 168 | time.Sleep(2 * time.Second) 169 | if err = cache.Replace("int", 3, time.Second); err != ErrNotStored { 170 | t.Errorf("Expected ErrNotStored, got: %s", err) 171 | } 172 | if err = cache.Get("int", &i); err != ErrCacheMiss { 173 | t.Errorf("Expected cache miss, got: %s", err) 174 | } 175 | } 176 | 177 | func testAdd(t *testing.T, newCache cacheFactory) { 178 | var err error 179 | cache := newCache(t, time.Hour) 180 | // Add to an empty cache. 181 | if err = cache.Add("int", 1, time.Second*3); err != nil { 182 | t.Errorf("Unexpected error adding to empty cache: %s", err) 183 | } 184 | 185 | // Try to add again. (fail) 186 | if err = cache.Add("int", 2, time.Second*3); err != nil { 187 | if err != ErrNotStored { 188 | t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err) 189 | } 190 | } 191 | 192 | // Wait for it to expire, and add again. 193 | time.Sleep(6 * time.Second) 194 | if err = cache.Add("int", 3, time.Second*3); err != nil { 195 | t.Errorf("Unexpected error adding to cache: %s", err) 196 | } 197 | 198 | // Get and verify the value. 199 | var i int 200 | if err = cache.Get("int", &i); err != nil { 201 | t.Errorf("Unexpected error: %s", err) 202 | } 203 | if i != 3 { 204 | t.Errorf("Expected 3, got: %d", i) 205 | } 206 | } 207 | 208 | func testGetMulti(t *testing.T, newCache cacheFactory) { 209 | cache := newCache(t, time.Hour) 210 | 211 | m := map[string]interface{}{ 212 | "str": "foo", 213 | "num": 42, 214 | "foo": struct{ Bar string }{"baz"}, 215 | } 216 | 217 | var keys []string 218 | for key, value := range m { 219 | keys = append(keys, key) 220 | if err := cache.Set(key, value, time.Second*3); err != nil { 221 | t.Errorf("Error setting a value: %s", err) 222 | } 223 | } 224 | 225 | g, err := cache.GetMulti(keys...) 226 | if err != nil { 227 | t.Errorf("Error in get-multi: %s", err) 228 | } 229 | 230 | var str string 231 | if err = g.Get("str", &str); err != nil || str != "foo" { 232 | t.Errorf("Error getting str: %s / %s", err, str) 233 | } 234 | 235 | var num int 236 | if err = g.Get("num", &num); err != nil || num != 42 { 237 | t.Errorf("Error getting num: %s / %v", err, num) 238 | } 239 | 240 | var foo struct{ Bar string } 241 | if err = g.Get("foo", &foo); err != nil || foo.Bar != "baz" { 242 | t.Errorf("Error getting foo: %s / %v", err, foo) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "golang.org/x/net/websocket" 14 | ) 15 | 16 | var ( 17 | MainRouter *Router 18 | MainTemplateLoader *TemplateLoader 19 | MainWatcher *Watcher 20 | Server *http.Server 21 | ) 22 | 23 | // This method handles all requests. It dispatches to handleInternal after 24 | // handling / adapting websocket connections. 25 | func handle(w http.ResponseWriter, r *http.Request) { 26 | if maxRequestSize := int64(Config.IntDefault("http.maxrequestsize", 0)); maxRequestSize > 0 { 27 | r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 28 | } 29 | 30 | upgrade := r.Header.Get("Upgrade") 31 | if upgrade == "websocket" || upgrade == "Websocket" { 32 | websocket.Handler(func(ws *websocket.Conn) { 33 | //Override default Read/Write timeout with sane value for a web socket request 34 | ws.SetDeadline(time.Now().Add(time.Hour * 24)) 35 | r.Method = "WS" 36 | handleInternal(w, r, ws) 37 | }).ServeHTTP(w, r) 38 | } else { 39 | handleInternal(w, r, nil) 40 | } 41 | } 42 | 43 | func handleInternal(w http.ResponseWriter, r *http.Request, ws *websocket.Conn) { 44 | // TODO For now this okay to put logger here for all the requests 45 | // However, it's best to have logging handler at server entry level 46 | start := time.Now() 47 | 48 | var ( 49 | req = NewRequest(r) 50 | resp = NewResponse(w) 51 | c = NewController(req, resp) 52 | ) 53 | req.Websocket = ws 54 | 55 | Filters[0](c, Filters[1:]) 56 | if c.Result != nil { 57 | c.Result.Apply(req, resp) 58 | } else if c.Response.Status != 0 { 59 | c.Response.Out.WriteHeader(c.Response.Status) 60 | } 61 | // Close the Writer if we can 62 | if w, ok := resp.Out.(io.Closer); ok { 63 | w.Close() 64 | } 65 | 66 | // Revel request access log format 67 | // RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath 68 | // Sample format: 69 | // 2016/05/25 17:46:37.112 127.0.0.1 200 270.157µs GET / 70 | requestLog.Printf("%v %v %v %10v %v %v", 71 | start.Format(requestLogTimeFormat), 72 | ClientIP(r), 73 | c.Response.Status, 74 | time.Since(start), 75 | r.Method, 76 | r.URL.Path, 77 | ) 78 | } 79 | 80 | // InitServer intializes the server and returns the handler 81 | // It can be used as an alternative entry-point if one needs the http handler 82 | // to be exposed. E.g. to run on multiple addresses and ports or to set custom 83 | // TLS options. 84 | func InitServer() http.HandlerFunc { 85 | runStartupHooks() 86 | 87 | // Load templates 88 | MainTemplateLoader = NewTemplateLoader(TemplatePaths) 89 | MainTemplateLoader.Refresh() 90 | 91 | // The "watch" config variable can turn on and off all watching. 92 | // (As a convenient way to control it all together.) 93 | if Config.BoolDefault("watch", true) { 94 | MainWatcher = NewWatcher() 95 | Filters = append([]Filter{WatchFilter}, Filters...) 96 | } 97 | 98 | // If desired (or by default), create a watcher for templates and routes. 99 | // The watcher calls Refresh() on things on the first request. 100 | if MainWatcher != nil && Config.BoolDefault("watch.templates", true) { 101 | MainWatcher.Listen(MainTemplateLoader, MainTemplateLoader.paths...) 102 | } 103 | 104 | return http.HandlerFunc(handle) 105 | } 106 | 107 | // Run the server. 108 | // This is called from the generated main file. 109 | // If port is non-zero, use that. Else, read the port from app.conf. 110 | func Run(port int) { 111 | address := HttpAddr 112 | if port == 0 { 113 | port = HttpPort 114 | } 115 | 116 | var network = "tcp" 117 | var localAddress string 118 | 119 | // If the port is zero, treat the address as a fully qualified local address. 120 | // This address must be prefixed with the network type followed by a colon, 121 | // e.g. unix:/tmp/app.socket or tcp6:::1 (equivalent to tcp6:0:0:0:0:0:0:0:1) 122 | if port == 0 { 123 | parts := strings.SplitN(address, ":", 2) 124 | network = parts[0] 125 | localAddress = parts[1] 126 | } else { 127 | localAddress = address + ":" + strconv.Itoa(port) 128 | } 129 | 130 | Server = &http.Server{ 131 | Addr: localAddress, 132 | Handler: http.HandlerFunc(handle), 133 | ReadTimeout: time.Duration(Config.IntDefault("timeout.read", 0)) * time.Second, 134 | WriteTimeout: time.Duration(Config.IntDefault("timeout.write", 0)) * time.Second, 135 | } 136 | 137 | InitServer() 138 | 139 | go func() { 140 | time.Sleep(100 * time.Millisecond) 141 | fmt.Printf("Listening on %s...\n", Server.Addr) 142 | }() 143 | 144 | if HttpSsl { 145 | if network != "tcp" { 146 | // This limitation is just to reduce complexity, since it is standard 147 | // to terminate SSL upstream when using unix domain sockets. 148 | ERROR.Fatalln("SSL is only supported for TCP sockets. Specify a port to listen on.") 149 | } 150 | ERROR.Fatalln("Failed to listen:", 151 | Server.ListenAndServeTLS(HttpSslCert, HttpSslKey)) 152 | } else { 153 | listener, err := net.Listen(network, Server.Addr) 154 | if err != nil { 155 | ERROR.Fatalln("Failed to listen:", err) 156 | } 157 | ERROR.Fatalln("Failed to serve:", Server.Serve(listener)) 158 | } 159 | } 160 | 161 | func runStartupHooks() { 162 | sort.Sort(startupHooks) 163 | for _, hook := range startupHooks { 164 | hook.f() 165 | } 166 | } 167 | 168 | type StartupHook struct { 169 | order int 170 | f func() 171 | } 172 | 173 | type StartupHooks []StartupHook 174 | 175 | var startupHooks StartupHooks 176 | 177 | func (slice StartupHooks) Len() int { 178 | return len(slice) 179 | } 180 | 181 | func (slice StartupHooks) Less(i, j int) bool { 182 | return slice[i].order < slice[j].order 183 | } 184 | 185 | func (slice StartupHooks) Swap(i, j int) { 186 | slice[i], slice[j] = slice[j], slice[i] 187 | } 188 | 189 | // Register a function to be run at app startup. 190 | // 191 | // The order you register the functions will be the order they are run. 192 | // You can think of it as a FIFO queue. 193 | // This process will happen after the config file is read 194 | // and before the server is listening for connections. 195 | // 196 | // Ideally, your application should have only one call to init() in the file init.go. 197 | // The reason being that the call order of multiple init() functions in 198 | // the same package is undefined. 199 | // Inside of init() call revel.OnAppStart() for each function you wish to register. 200 | // 201 | // Example: 202 | // 203 | // // from: yourapp/app/controllers/somefile.go 204 | // func InitDB() { 205 | // // do DB connection stuff here 206 | // } 207 | // 208 | // func FillCache() { 209 | // // fill a cache from DB 210 | // // this depends on InitDB having been run 211 | // } 212 | // 213 | // // from: yourapp/app/init.go 214 | // func init() { 215 | // // set up filters... 216 | // 217 | // // register startup functions 218 | // revel.OnAppStart(InitDB) 219 | // revel.OnAppStart(FillCache) 220 | // } 221 | // 222 | // This can be useful when you need to establish connections to databases or third-party services, 223 | // setup app components, compile assets, or any thing you need to do between starting Revel and accepting connections. 224 | // 225 | func OnAppStart(f func(), order ...int) { 226 | o := 1 227 | if len(order) > 0 { 228 | o = order[0] 229 | } 230 | startupHooks = append(startupHooks, StartupHook{order: o, f: f}) 231 | } 232 | -------------------------------------------------------------------------------- /cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | "github.com/revel/revel" 8 | ) 9 | 10 | // Wraps the Redis client to meet the Cache interface. 11 | type RedisCache struct { 12 | pool *redis.Pool 13 | defaultExpiration time.Duration 14 | } 15 | 16 | // until redigo supports sharding/clustering, only one host will be in hostList 17 | func NewRedisCache(host string, password string, defaultExpiration time.Duration) RedisCache { 18 | var pool = &redis.Pool{ 19 | MaxIdle: revel.Config.IntDefault("cache.redis.maxidle", 5), 20 | MaxActive: revel.Config.IntDefault("cache.redis.maxactive", 0), 21 | IdleTimeout: time.Duration(revel.Config.IntDefault("cache.redis.idletimeout", 240)) * time.Second, 22 | Dial: func() (redis.Conn, error) { 23 | protocol := revel.Config.StringDefault("cache.redis.protocol", "tcp") 24 | toc := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.connect", 10000)) 25 | tor := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.read", 5000)) 26 | tow := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.write", 5000)) 27 | c, err := redis.DialTimeout(protocol, host, toc, tor, tow) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if len(password) > 0 { 32 | if _, err := c.Do("AUTH", password); err != nil { 33 | c.Close() 34 | return nil, err 35 | } 36 | } else { 37 | // check with PING 38 | if _, err := c.Do("PING"); err != nil { 39 | c.Close() 40 | return nil, err 41 | } 42 | } 43 | return c, err 44 | }, 45 | // custom connection test method 46 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 47 | if _, err := c.Do("PING"); err != nil { 48 | return err 49 | } 50 | return nil 51 | }, 52 | } 53 | return RedisCache{pool, defaultExpiration} 54 | } 55 | 56 | func (c RedisCache) Set(key string, value interface{}, expires time.Duration) error { 57 | conn := c.pool.Get() 58 | defer conn.Close() 59 | return c.invoke(conn.Do, key, value, expires) 60 | } 61 | 62 | func (c RedisCache) Add(key string, value interface{}, expires time.Duration) error { 63 | conn := c.pool.Get() 64 | defer conn.Close() 65 | existed, err := exists(conn, key) 66 | if err != nil { 67 | return err 68 | } else if existed { 69 | return ErrNotStored 70 | } 71 | return c.invoke(conn.Do, key, value, expires) 72 | } 73 | 74 | func (c RedisCache) Replace(key string, value interface{}, expires time.Duration) error { 75 | conn := c.pool.Get() 76 | defer conn.Close() 77 | existed, err := exists(conn, key) 78 | if err != nil { 79 | return err 80 | } else if !existed { 81 | return ErrNotStored 82 | } 83 | err = c.invoke(conn.Do, key, value, expires) 84 | if value == nil { 85 | return ErrNotStored 86 | } else { 87 | return err 88 | } 89 | } 90 | 91 | func (c RedisCache) Get(key string, ptrValue interface{}) error { 92 | conn := c.pool.Get() 93 | defer conn.Close() 94 | raw, err := conn.Do("GET", key) 95 | if err != nil { 96 | return err 97 | } else if raw == nil { 98 | return ErrCacheMiss 99 | } 100 | item, err := redis.Bytes(raw, err) 101 | if err != nil { 102 | return err 103 | } 104 | return Deserialize(item, ptrValue) 105 | } 106 | 107 | func generalizeStringSlice(strs []string) []interface{} { 108 | ret := make([]interface{}, len(strs)) 109 | for i, str := range strs { 110 | ret[i] = str 111 | } 112 | return ret 113 | } 114 | 115 | func (c RedisCache) GetMulti(keys ...string) (Getter, error) { 116 | conn := c.pool.Get() 117 | defer conn.Close() 118 | 119 | items, err := redis.Values(conn.Do("MGET", generalizeStringSlice(keys)...)) 120 | if err != nil { 121 | return nil, err 122 | } else if items == nil { 123 | return nil, ErrCacheMiss 124 | } 125 | 126 | m := make(map[string][]byte) 127 | for i, key := range keys { 128 | m[key] = nil 129 | if i < len(items) && items[i] != nil { 130 | s, ok := items[i].([]byte) 131 | if ok { 132 | m[key] = s 133 | } 134 | } 135 | } 136 | return RedisItemMapGetter(m), nil 137 | } 138 | 139 | func exists(conn redis.Conn, key string) (bool, error) { 140 | return redis.Bool(conn.Do("EXISTS", key)) 141 | } 142 | 143 | func (c RedisCache) Delete(key string) error { 144 | conn := c.pool.Get() 145 | defer conn.Close() 146 | existed, err := redis.Bool(conn.Do("DEL", key)) 147 | if err == nil && !existed { 148 | err = ErrCacheMiss 149 | } 150 | return err 151 | } 152 | 153 | func (c RedisCache) Increment(key string, delta uint64) (uint64, error) { 154 | conn := c.pool.Get() 155 | defer conn.Close() 156 | // Check for existence *before* increment as per the cache contract. 157 | // redis will auto create the key, and we don't want that. Since we need to do increment 158 | // ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value 159 | // and do the exists check this way to minimize calls to Redis 160 | val, err := conn.Do("GET", key) 161 | if err != nil { 162 | return 0, err 163 | } else if val == nil { 164 | return 0, ErrCacheMiss 165 | } 166 | currentVal, err := redis.Int64(val, nil) 167 | if err != nil { 168 | return 0, err 169 | } 170 | var sum int64 = currentVal + int64(delta) 171 | _, err = conn.Do("SET", key, sum) 172 | if err != nil { 173 | return 0, err 174 | } 175 | return uint64(sum), nil 176 | } 177 | 178 | func (c RedisCache) Decrement(key string, delta uint64) (newValue uint64, err error) { 179 | conn := c.pool.Get() 180 | defer conn.Close() 181 | // Check for existence *before* increment as per the cache contract. 182 | // redis will auto create the key, and we don't want that, hence the exists call 183 | existed, err := exists(conn, key) 184 | if err != nil { 185 | return 0, err 186 | } else if !existed { 187 | return 0, ErrCacheMiss 188 | } 189 | // Decrement contract says you can only go to 0 190 | // so we go fetch the value and if the delta is greater than the amount, 191 | // 0 out the value 192 | currentVal, err := redis.Int64(conn.Do("GET", key)) 193 | if err != nil { 194 | return 0, err 195 | } 196 | if delta > uint64(currentVal) { 197 | tempint, err := redis.Int64(conn.Do("DECRBY", key, currentVal)) 198 | return uint64(tempint), err 199 | } 200 | tempint, err := redis.Int64(conn.Do("DECRBY", key, delta)) 201 | return uint64(tempint), err 202 | } 203 | 204 | func (c RedisCache) Flush() error { 205 | conn := c.pool.Get() 206 | defer conn.Close() 207 | _, err := conn.Do("FLUSHALL") 208 | return err 209 | } 210 | 211 | func (c RedisCache) invoke(f func(string, ...interface{}) (interface{}, error), 212 | key string, value interface{}, expires time.Duration) error { 213 | 214 | switch expires { 215 | case DEFAULT: 216 | expires = c.defaultExpiration 217 | case FOREVER: 218 | expires = time.Duration(0) 219 | } 220 | 221 | b, err := Serialize(value) 222 | if err != nil { 223 | return err 224 | } 225 | conn := c.pool.Get() 226 | defer conn.Close() 227 | if expires > 0 { 228 | _, err := f("SETEX", key, int32(expires/time.Second), b) 229 | return err 230 | } else { 231 | _, err := f("SET", key, b) 232 | return err 233 | } 234 | } 235 | 236 | // Implement a Getter on top of the returned item map. 237 | type RedisItemMapGetter map[string][]byte 238 | 239 | func (g RedisItemMapGetter) Get(key string, ptrValue interface{}) error { 240 | item, ok := g[key] 241 | if !ok { 242 | return ErrCacheMiss 243 | } 244 | return Deserialize(item, ptrValue) 245 | } 246 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Revel 2 | 3 | This describes how developers may contribute to Revel. 4 | 5 | ## Mission 6 | 7 | Revel's mission is to provide a batteries-included framework for making large 8 | scale web application development as efficient and maintainable as possible. 9 | 10 | The design should be configurable and modular so that it can grow with the 11 | developer. However, it should provide a wonderful un-boxing experience and 12 | default configuration that can woo new developers and make simple web apps 13 | straight forward. The framework should have an opinion about how to do all of the 14 | common tasks in web development to reduce unnecessary cognitive load. 15 | 16 | Perhaps most important of all, Revel should be a joy to use. We want to reduce 17 | the time spent on tedious boilerplate functionality and increase the time 18 | available for creating polished solutions for your application's target users. 19 | 20 | ## How to Contribute 21 | 22 | ### Join the Community 23 | 24 | The first step to making Revel better is joining the community! You can find the 25 | community on: 26 | 27 | * [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) via [revel-framework@googlegroups.com](mailto:revel-framework@googlegroups.com) 28 | * [GitHub Issues](https://github.com/revel/revel/issues) 29 | * [StackOverflow Questions](http://stackoverflow.com/questions/tagged/revel) 30 | * [IRC](http://webchat.freenode.net/?channels=%23revel&uio=d4) via #revel on Freenode 31 | 32 | Once you've joined, there are many ways to contribute to Revel: 33 | 34 | * Report bugs (via GitHub) 35 | * Answer questions of other community members (via Google Groups or IRC) 36 | * Give feedback on new feature discussions (via GitHub and Google Groups) 37 | * Propose your own ideas (via Google Groups or GitHub) 38 | 39 | ### How Revel is Developed 40 | 41 | We have begun to formalize the development process by adopting pragmatic 42 | practices such as: 43 | 44 | * Developing on the `develop` branch 45 | * Merging `develop` branch to `master` branch in 6 week iterations 46 | * Tagging releases with MAJOR.MINOR syntax (e.g. v0.8) 47 | ** We may also tag MAJOR.MINOR.HOTFIX releases as needed (e.g. v0.8.1) to 48 | address urgent bugs. Such releases will not introduce or change functionality 49 | * Managing bugs, enhancements, features and release milestones via GitHub's Issue Tracker 50 | * Using feature branches to create pull requests 51 | * Discussing new features **before** hacking away at it 52 | 53 | 54 | ### How to Correctly Fork 55 | 56 | Go uses the repository URL to import packages, so forking and `go get`ing the 57 | forked project **will not work**. 58 | 59 | Instead, follow these steps: 60 | 61 | 1. Install Revel normally 62 | 2. Fork Revel on GitHub 63 | 3. Add your fork as a git remote 64 | 65 | Here's the commands to do so: 66 | ``` 67 | $ go get github.com/revel/revel # Install Revel 68 | $ cd $GOPATH/src/github.com/revel/revel # Change directory to revel repo 69 | $ git remote add fork git@github.com:$USER/revel.git # Add your fork as a remote, where $USER is your GitHub username 70 | ``` 71 | 72 | ### Create a Feature Branch & Code Away! 73 | 74 | Now that you've properly installed and forked Revel, you are ready to start coding (assuming 75 | you have a validated your ideas with other community members)! 76 | 77 | In order to have your pull requests accepted, we recommend you make your changes to Revel on a 78 | new git branch. For example, 79 | ``` 80 | $ git checkout -b feature/useful-new-thing develop # Create a new branch based on develop and switch to it 81 | $ ... # Make your changes and commit them 82 | $ git push fork develop # After new commits, push to your fork 83 | ``` 84 | 85 | ### Format Your Code 86 | 87 | Remember to run `go fmt` before committing your changes. 88 | Many Go developers opt to have their editor run `go fmt` automatically when 89 | saving Go files. 90 | 91 | Additionally, follow the [core Go style conventions](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 92 | to have your pull requests accepted. 93 | 94 | ### Write Tests (and Benchmarks for Bonus Points) 95 | 96 | Significant new features require tests. Besides unit tests, it is also possible 97 | to test a feature by exercising it in one of the sample apps and verifying its 98 | operation using that app's test suite. This has the added benefit of providing 99 | example code for developers to refer to. 100 | 101 | Benchmarks are helpful but not required. 102 | 103 | ### Run the Tests 104 | 105 | Typically running the main set of unit tests will be sufficient: 106 | 107 | ``` 108 | $ go test github.com/revel/revel 109 | ``` 110 | 111 | Refer to the 112 | [Travis configuration](https://github.com/revel/revel/blob/master/.travis.yml) 113 | for the full set of tests. They take less than a minute to run. 114 | 115 | ### Document Your Feature 116 | 117 | Due to the wide audience and shared nature of Revel, documentation is an essential 118 | addition to your new code. **Pull requests risk not being accepted** until proper 119 | documentation is created to detail how to make use of new functionality. 120 | 121 | The [Revel web site](http://revel.github.io/) is hosted on GitHub Pages and 122 | [built with Jekyll](https://help.github.com/articles/using-jekyll-with-pages). 123 | 124 | To develop the Jekyll site locally: 125 | 126 | # Clone the documentation repository 127 | $ git clone git@github.com:revel/revel.github.io 128 | $ cd revel.github.io 129 | 130 | # Install and run Jekyll to generate and serve the site 131 | $ gem install jekyll kramdown 132 | $ jekyll serve --watch 133 | 134 | # Now load in your browser 135 | $ open http://localhost:4000/ 136 | 137 | Any changes you make to the site should be reflected within a few seconds. 138 | 139 | ### Submit Pull Request 140 | 141 | Once you've done all of the above & pushed your changes to your fork, you can create a pull request for review and acceptance. 142 | 143 | ## Potential Projects 144 | 145 | These are outstanding feature requests, roughly ordered by priority. 146 | Additionally, there are frequently smaller feature requests or items in the 147 | [issues](https://github.com/revel/revel/issues?labels=contributor+ready&page=1&state=open). 148 | 149 | 1. Better ORM support. Provide more samples (or modules) and better documentation for setting up common situations like SQL database, Mongo, LevelDB, etc. 150 | 1. Support for other templating languages (e.g. mustache, HAML). Make TemplateLoader pluggable. Use Pongo instead of vanilla Go templates (and update the samples) 151 | 1. Test Fixtures 152 | 1. Authenticity tokens for CSRF protection 153 | 1. Coffeescript pre-processor. Could potentially use [otto](https://github.com/robertkrimen/otto) as a native Go method to compiling. 154 | 1. SCSS/LESS pre-processor. 155 | 1. GAE support. Some progress made in the 'appengine' branch -- the remaining piece is running the appengine services in development. 156 | 1. More Form helpers (template funcs). 157 | 1. A Mongo module (perhaps with a sample app) 158 | 1. Deployment to OpenShift (support, documentation, etc) 159 | 1. Improve the logging situation. The configuration is a little awkward and not very powerful. Integrating something more powerful would be good. (like [seelog](https://github.com/cihub/seelog) or [log4go](https://code.google.com/p/log4go/)) 160 | 1. ETags, cache controls 161 | 1. A module or plugins for adding HTTP Basic Auth 162 | 1. Allowing the app to hook into the source code processing step 163 | -------------------------------------------------------------------------------- /filterconfig.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // Map from "Controller" or "Controller.Method" to the Filter chain 9 | var filterOverrides = make(map[string][]Filter) 10 | 11 | // FilterConfigurator allows the developer configure the filter chain on a 12 | // per-controller or per-action basis. The filter configuration is applied by 13 | // the FilterConfiguringFilter, which is itself a filter stage. For example, 14 | // 15 | // Assuming: 16 | // Filters = []Filter{ 17 | // RouterFilter, 18 | // FilterConfiguringFilter, 19 | // SessionFilter, 20 | // ActionInvoker, 21 | // } 22 | // 23 | // Add: 24 | // FilterAction(App.Action). 25 | // Add(OtherFilter) 26 | // 27 | // => RouterFilter, FilterConfiguringFilter, SessionFilter, OtherFilter, ActionInvoker 28 | // 29 | // Remove: 30 | // FilterAction(App.Action). 31 | // Remove(SessionFilter) 32 | // 33 | // => RouterFilter, FilterConfiguringFilter, OtherFilter, ActionInvoker 34 | // 35 | // Insert: 36 | // FilterAction(App.Action). 37 | // Insert(OtherFilter, revel.BEFORE, SessionFilter) 38 | // 39 | // => RouterFilter, FilterConfiguringFilter, OtherFilter, SessionFilter, ActionInvoker 40 | // 41 | // Filter modifications may be combined between Controller and Action. For example: 42 | // FilterController(App{}). 43 | // Add(Filter1) 44 | // FilterAction(App.Action). 45 | // Add(Filter2) 46 | // 47 | // .. would result in App.Action being filtered by both Filter1 and Filter2. 48 | // 49 | // Note: the last filter stage is not subject to the configurator. In 50 | // particular, Add() adds a filter to the second-to-last place. 51 | type FilterConfigurator struct { 52 | key string // e.g. "App", "App.Action" 53 | controllerName string // e.g. "App" 54 | } 55 | 56 | func newFilterConfigurator(controllerName, methodName string) FilterConfigurator { 57 | if methodName == "" { 58 | return FilterConfigurator{controllerName, controllerName} 59 | } 60 | return FilterConfigurator{controllerName + "." + methodName, controllerName} 61 | } 62 | 63 | // FilterController returns a configurator for the filters applied to all 64 | // actions on the given controller instance. For example: 65 | // FilterAction(MyController{}) 66 | func FilterController(controllerInstance interface{}) FilterConfigurator { 67 | t := reflect.TypeOf(controllerInstance) 68 | for t.Kind() == reflect.Ptr { 69 | t = t.Elem() 70 | } 71 | return newFilterConfigurator(t.Name(), "") 72 | } 73 | 74 | // FilterAction returns a configurator for the filters applied to the given 75 | // controller method. For example: 76 | // FilterAction(MyController.MyAction) 77 | func FilterAction(methodRef interface{}) FilterConfigurator { 78 | var ( 79 | methodValue = reflect.ValueOf(methodRef) 80 | methodType = methodValue.Type() 81 | ) 82 | if methodType.Kind() != reflect.Func || methodType.NumIn() == 0 { 83 | panic("Expecting a controller method reference (e.g. Controller.Action), got a " + 84 | methodType.String()) 85 | } 86 | 87 | controllerType := methodType.In(0) 88 | method := FindMethod(controllerType, methodValue) 89 | if method == nil { 90 | panic("Action not found on controller " + controllerType.Name()) 91 | } 92 | 93 | for controllerType.Kind() == reflect.Ptr { 94 | controllerType = controllerType.Elem() 95 | } 96 | 97 | return newFilterConfigurator(controllerType.Name(), method.Name) 98 | } 99 | 100 | // Add the given filter in the second-to-last position in the filter chain. 101 | // (Second-to-last so that it is before ActionInvoker) 102 | func (conf FilterConfigurator) Add(f Filter) FilterConfigurator { 103 | conf.apply(func(fc []Filter) []Filter { 104 | return conf.addFilter(f, fc) 105 | }) 106 | return conf 107 | } 108 | 109 | func (conf FilterConfigurator) addFilter(f Filter, fc []Filter) []Filter { 110 | return append(fc[:len(fc)-1], f, fc[len(fc)-1]) 111 | } 112 | 113 | // Remove a filter from the filter chain. 114 | func (conf FilterConfigurator) Remove(target Filter) FilterConfigurator { 115 | conf.apply(func(fc []Filter) []Filter { 116 | return conf.rmFilter(target, fc) 117 | }) 118 | return conf 119 | } 120 | 121 | func (conf FilterConfigurator) rmFilter(target Filter, fc []Filter) []Filter { 122 | for i, f := range fc { 123 | if FilterEq(f, target) { 124 | return append(fc[:i], fc[i+1:]...) 125 | } 126 | } 127 | return fc 128 | } 129 | 130 | // Insert a filter into the filter chain before or after another. 131 | // This may be called with the BEFORE or AFTER constants, for example: 132 | // revel.FilterAction(App.Index). 133 | // Insert(MyFilter, revel.BEFORE, revel.ActionInvoker). 134 | // Insert(MyFilter2, revel.AFTER, revel.PanicFilter) 135 | func (conf FilterConfigurator) Insert(insert Filter, where When, target Filter) FilterConfigurator { 136 | if where != BEFORE && where != AFTER { 137 | panic("where must be BEFORE or AFTER") 138 | } 139 | conf.apply(func(fc []Filter) []Filter { 140 | return conf.insertFilter(insert, where, target, fc) 141 | }) 142 | return conf 143 | } 144 | 145 | func (conf FilterConfigurator) insertFilter(insert Filter, where When, target Filter, fc []Filter) []Filter { 146 | for i, f := range fc { 147 | if FilterEq(f, target) { 148 | if where == BEFORE { 149 | return append(fc[:i], append([]Filter{insert}, fc[i:]...)...) 150 | } else { 151 | return append(fc[:i+1], append([]Filter{insert}, fc[i+1:]...)...) 152 | } 153 | } 154 | } 155 | return fc 156 | } 157 | 158 | // getChain returns the filter chain that applies to the given controller or 159 | // action. If no overrides are configured, then a copy of the default filter 160 | // chain is returned. 161 | func (conf FilterConfigurator) getChain() []Filter { 162 | var filters []Filter 163 | if filters = getOverrideChain(conf.controllerName, conf.key); filters == nil { 164 | // The override starts with all filters after FilterConfiguringFilter 165 | for i, f := range Filters { 166 | if FilterEq(f, FilterConfiguringFilter) { 167 | filters = make([]Filter, len(Filters)-i-1) 168 | copy(filters, Filters[i+1:]) 169 | break 170 | } 171 | } 172 | if filters == nil { 173 | panic("FilterConfiguringFilter not found in revel.Filters.") 174 | } 175 | } 176 | return filters 177 | } 178 | 179 | // apply applies the given functional change to the filter overrides. 180 | // No other function modifies the filterOverrides map. 181 | func (conf FilterConfigurator) apply(f func([]Filter) []Filter) { 182 | // Updates any actions that have had their filters overridden, if this is a 183 | // Controller configurator. 184 | if conf.controllerName == conf.key { 185 | for k, v := range filterOverrides { 186 | if strings.HasPrefix(k, conf.controllerName+".") { 187 | filterOverrides[k] = f(v) 188 | } 189 | } 190 | } 191 | 192 | // Update the Controller or Action overrides. 193 | filterOverrides[conf.key] = f(conf.getChain()) 194 | } 195 | 196 | // FilterEq returns true if the two filters reference the same filter. 197 | func FilterEq(a, b Filter) bool { 198 | return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer() 199 | } 200 | 201 | // FilterConfiguringFilter is a filter stage that customizes the remaining 202 | // filter chain for the action being invoked. 203 | func FilterConfiguringFilter(c *Controller, fc []Filter) { 204 | if newChain := getOverrideChain(c.Name, c.Action); newChain != nil { 205 | newChain[0](c, newChain[1:]) 206 | return 207 | } 208 | fc[0](c, fc[1:]) 209 | } 210 | 211 | // getOverrideChain retrieves the overrides for the action that is set 212 | func getOverrideChain(controllerName, action string) []Filter { 213 | if newChain, ok := filterOverrides[action]; ok { 214 | return newChain 215 | } 216 | if newChain, ok := filterOverrides[controllerName]; ok { 217 | return newChain 218 | } 219 | return nil 220 | } 221 | -------------------------------------------------------------------------------- /i18n.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/revel/config" 11 | ) 12 | 13 | const ( 14 | CurrentLocaleRenderArg = "currentLocale" // The key for the current locale render arg value 15 | 16 | messageFilesDirectory = "messages" 17 | messageFilePattern = `^\w+\.[a-zA-Z]{2}$` 18 | defaultUnknownFormat = "??? %s ???" 19 | unknownFormatConfigKey = "i18n.unknown_format" 20 | defaultLanguageOption = "i18n.default_language" 21 | localeCookieConfigKey = "i18n.cookie" 22 | ) 23 | 24 | var ( 25 | // All currently loaded message configs. 26 | messages map[string]*config.Config 27 | ) 28 | 29 | // Setting MessageFunc allows you to override the translation interface. 30 | // 31 | // Set this to your own function that translates to the current locale. 32 | // This allows you to set up your own loading and logging of translated texts. 33 | // 34 | // See Message(...) in i18n.go for example of function. 35 | var MessageFunc func(locale, message string, args ...interface{}) (value string) = Message 36 | 37 | // Return all currently loaded message languages. 38 | func MessageLanguages() []string { 39 | languages := make([]string, len(messages)) 40 | i := 0 41 | for language, _ := range messages { 42 | languages[i] = language 43 | i++ 44 | } 45 | return languages 46 | } 47 | 48 | // Perform a message look-up for the given locale and message using the given arguments. 49 | // 50 | // When either an unknown locale or message is detected, a specially formatted string is returned. 51 | func Message(locale, message string, args ...interface{}) string { 52 | language, region := parseLocale(locale) 53 | unknownValueFormat := getUnknownValueFormat() 54 | 55 | messageConfig, knownLanguage := messages[language] 56 | if !knownLanguage { 57 | TRACE.Printf("Unsupported language for locale '%s' and message '%s', trying default language", locale, message) 58 | 59 | if defaultLanguage, found := Config.String(defaultLanguageOption); found { 60 | TRACE.Printf("Using default language '%s'", defaultLanguage) 61 | 62 | messageConfig, knownLanguage = messages[defaultLanguage] 63 | if !knownLanguage { 64 | WARN.Printf("Unsupported default language for locale '%s' and message '%s'", defaultLanguage, message) 65 | return fmt.Sprintf(unknownValueFormat, message) 66 | } 67 | } else { 68 | WARN.Printf("Unable to find default language option (%s); messages for unsupported locales will never be translated", defaultLanguageOption) 69 | return fmt.Sprintf(unknownValueFormat, message) 70 | } 71 | } 72 | 73 | // This works because unlike the goconfig documentation suggests it will actually 74 | // try to resolve message in DEFAULT if it did not find it in the given section. 75 | value, error := messageConfig.String(region, message) 76 | if error != nil { 77 | WARN.Printf("Unknown message '%s' for locale '%s'", message, locale) 78 | return fmt.Sprintf(unknownValueFormat, message) 79 | } 80 | 81 | if len(args) > 0 { 82 | TRACE.Printf("Arguments detected, formatting '%s' with %v", value, args) 83 | value = fmt.Sprintf(value, args...) 84 | } 85 | 86 | return value 87 | } 88 | 89 | func parseLocale(locale string) (language, region string) { 90 | if strings.Contains(locale, "-") { 91 | languageAndRegion := strings.Split(locale, "-") 92 | return languageAndRegion[0], languageAndRegion[1] 93 | } 94 | 95 | return locale, "" 96 | } 97 | 98 | // Retrieve message format or default format when i18n message is missing. 99 | func getUnknownValueFormat() string { 100 | return Config.StringDefault(unknownFormatConfigKey, defaultUnknownFormat) 101 | } 102 | 103 | // Recursively read and cache all available messages from all message files on the given path. 104 | func loadMessages(path string) { 105 | messages = make(map[string]*config.Config) 106 | 107 | // Read in messages from the modules. Load the module messges first, 108 | // so that it can be override in parent application 109 | for _, module := range Modules { 110 | TRACE.Println("Importing messages from module:", module.ImportPath) 111 | if err := Walk(filepath.Join(module.Path, messageFilesDirectory), loadMessageFile); err != nil && 112 | !os.IsNotExist(err) { 113 | ERROR.Println("Error reading messages files from module:", err) 114 | } 115 | } 116 | 117 | if err := Walk(path, loadMessageFile); err != nil && !os.IsNotExist(err) { 118 | ERROR.Println("Error reading messages files:", err) 119 | } 120 | } 121 | 122 | // Load a single message file 123 | func loadMessageFile(path string, info os.FileInfo, osError error) error { 124 | if osError != nil { 125 | return osError 126 | } 127 | if info.IsDir() { 128 | return nil 129 | } 130 | 131 | if matched, _ := regexp.MatchString(messageFilePattern, info.Name()); matched { 132 | if config, error := parseMessagesFile(path); error != nil { 133 | return error 134 | } else { 135 | locale := parseLocaleFromFileName(info.Name()) 136 | 137 | // If we have already parsed a message file for this locale, merge both 138 | if _, exists := messages[locale]; exists { 139 | messages[locale].Merge(config) 140 | TRACE.Printf("Successfully merged messages for locale '%s'", locale) 141 | } else { 142 | messages[locale] = config 143 | } 144 | 145 | TRACE.Println("Successfully loaded messages from file", info.Name()) 146 | } 147 | } else { 148 | TRACE.Printf("Ignoring file %s because it did not have a valid extension", info.Name()) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func parseMessagesFile(path string) (messageConfig *config.Config, error error) { 155 | messageConfig, error = config.ReadDefault(path) 156 | return 157 | } 158 | 159 | func parseLocaleFromFileName(file string) string { 160 | extension := filepath.Ext(file)[1:] 161 | return strings.ToLower(extension) 162 | } 163 | 164 | func init() { 165 | OnAppStart(func() { 166 | loadMessages(filepath.Join(BasePath, messageFilesDirectory)) 167 | }) 168 | } 169 | 170 | func I18nFilter(c *Controller, fc []Filter) { 171 | if foundCookie, cookieValue := hasLocaleCookie(c.Request); foundCookie { 172 | TRACE.Printf("Found locale cookie value: %s", cookieValue) 173 | setCurrentLocaleControllerArguments(c, cookieValue) 174 | } else if foundHeader, headerValue := hasAcceptLanguageHeader(c.Request); foundHeader { 175 | TRACE.Printf("Found Accept-Language header value: %s", headerValue) 176 | setCurrentLocaleControllerArguments(c, headerValue) 177 | } else { 178 | TRACE.Println("Unable to find locale in cookie or header, using empty string") 179 | setCurrentLocaleControllerArguments(c, "") 180 | } 181 | fc[0](c, fc[1:]) 182 | } 183 | 184 | // Set the current locale controller argument (CurrentLocaleControllerArg) with the given locale. 185 | func setCurrentLocaleControllerArguments(c *Controller, locale string) { 186 | c.Request.Locale = locale 187 | c.RenderArgs[CurrentLocaleRenderArg] = locale 188 | } 189 | 190 | // Determine whether the given request has valid Accept-Language value. 191 | // 192 | // Assumes that the accept languages stored in the request are sorted according to quality, with top 193 | // quality first in the slice. 194 | func hasAcceptLanguageHeader(request *Request) (bool, string) { 195 | if request.AcceptLanguages != nil && len(request.AcceptLanguages) > 0 { 196 | return true, request.AcceptLanguages[0].Language 197 | } 198 | 199 | return false, "" 200 | } 201 | 202 | // Determine whether the given request has a valid language cookie value. 203 | func hasLocaleCookie(request *Request) (bool, string) { 204 | if request != nil && request.Cookies() != nil { 205 | name := Config.StringDefault(localeCookieConfigKey, CookiePrefix+"_LANG") 206 | if cookie, error := request.Cookie(name); error == nil { 207 | return true, cookie.Value 208 | } else { 209 | TRACE.Printf("Unable to read locale cookie with name '%s': %s", name, error.Error()) 210 | } 211 | } 212 | 213 | return false, "" 214 | } 215 | -------------------------------------------------------------------------------- /validation.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "regexp" 8 | "runtime" 9 | ) 10 | 11 | // Simple struct to store the Message & Key of a validation error 12 | type ValidationError struct { 13 | Message, Key string 14 | } 15 | 16 | // String returns the Message field of the ValidationError struct. 17 | func (e *ValidationError) String() string { 18 | if e == nil { 19 | return "" 20 | } 21 | return e.Message 22 | } 23 | 24 | // A Validation context manages data validation and error messages. 25 | type Validation struct { 26 | Errors []*ValidationError 27 | keep bool 28 | } 29 | 30 | // Keep tells revel to set a flash cookie on the client to make the validation 31 | // errors available for the next request. 32 | // This is helpful when redirecting the client after the validation failed. 33 | // It is good practice to always redirect upon a HTTP POST request. Thus 34 | // one should use this method when HTTP POST validation failed and redirect 35 | // the user back to the form. 36 | func (v *Validation) Keep() { 37 | v.keep = true 38 | } 39 | 40 | // Clear *all* ValidationErrors 41 | func (v *Validation) Clear() { 42 | v.Errors = []*ValidationError{} 43 | } 44 | 45 | // HasErrors returns true if there are any (ie > 0) errors. False otherwise. 46 | func (v *Validation) HasErrors() bool { 47 | return len(v.Errors) > 0 48 | } 49 | 50 | // ErrorMap returns the errors mapped by key. 51 | // If there are multiple validation errors associated with a single key, the 52 | // first one "wins". (Typically the first validation will be the more basic). 53 | func (v *Validation) ErrorMap() map[string]*ValidationError { 54 | m := map[string]*ValidationError{} 55 | for _, e := range v.Errors { 56 | if _, ok := m[e.Key]; !ok { 57 | m[e.Key] = e 58 | } 59 | } 60 | return m 61 | } 62 | 63 | // Error adds an error to the validation context. 64 | func (v *Validation) Error(message string, args ...interface{}) *ValidationResult { 65 | result := (&ValidationResult{ 66 | Ok: false, 67 | Error: &ValidationError{}, 68 | }).Message(message, args...) 69 | v.Errors = append(v.Errors, result.Error) 70 | return result 71 | } 72 | 73 | // A ValidationResult is returned from every validation method. 74 | // It provides an indication of success, and a pointer to the Error (if any). 75 | type ValidationResult struct { 76 | Error *ValidationError 77 | Ok bool 78 | } 79 | 80 | // Key sets the ValidationResult's Error "key" and returns itself for chaining 81 | func (r *ValidationResult) Key(key string) *ValidationResult { 82 | if r.Error != nil { 83 | r.Error.Key = key 84 | } 85 | return r 86 | } 87 | 88 | // Message sets the error message for a ValidationResult. Returns itself to 89 | // allow chaining. Allows Sprintf() type calling with multiple parameters 90 | func (r *ValidationResult) Message(message string, args ...interface{}) *ValidationResult { 91 | if r.Error != nil { 92 | if len(args) == 0 { 93 | r.Error.Message = message 94 | } else { 95 | r.Error.Message = fmt.Sprintf(message, args...) 96 | } 97 | } 98 | return r 99 | } 100 | 101 | // Required tests that the argument is non-nil and non-empty (if string or list) 102 | func (v *Validation) Required(obj interface{}) *ValidationResult { 103 | return v.apply(Required{}, obj) 104 | } 105 | 106 | func (v *Validation) Min(n int, min int) *ValidationResult { 107 | return v.apply(Min{min}, n) 108 | } 109 | 110 | func (v *Validation) Max(n int, max int) *ValidationResult { 111 | return v.apply(Max{max}, n) 112 | } 113 | 114 | func (v *Validation) Range(n, min, max int) *ValidationResult { 115 | return v.apply(Range{Min{min}, Max{max}}, n) 116 | } 117 | 118 | func (v *Validation) MinSize(obj interface{}, min int) *ValidationResult { 119 | return v.apply(MinSize{min}, obj) 120 | } 121 | 122 | func (v *Validation) MaxSize(obj interface{}, max int) *ValidationResult { 123 | return v.apply(MaxSize{max}, obj) 124 | } 125 | 126 | func (v *Validation) Length(obj interface{}, n int) *ValidationResult { 127 | return v.apply(Length{n}, obj) 128 | } 129 | 130 | func (v *Validation) Match(str string, regex *regexp.Regexp) *ValidationResult { 131 | return v.apply(Match{regex}, str) 132 | } 133 | 134 | func (v *Validation) Email(str string) *ValidationResult { 135 | return v.apply(Email{Match{emailPattern}}, str) 136 | } 137 | 138 | func (v *Validation) apply(chk Validator, obj interface{}) *ValidationResult { 139 | if chk.IsSatisfied(obj) { 140 | return &ValidationResult{Ok: true} 141 | } 142 | 143 | // Get the default key. 144 | var key string 145 | if pc, _, line, ok := runtime.Caller(2); ok { 146 | f := runtime.FuncForPC(pc) 147 | if defaultKeys, ok := DefaultValidationKeys[f.Name()]; ok { 148 | key = defaultKeys[line] 149 | } 150 | } else { 151 | INFO.Println("Failed to get Caller information to look up Validation key") 152 | } 153 | 154 | // Add the error to the validation context. 155 | err := &ValidationError{ 156 | Message: chk.DefaultMessage(), 157 | Key: key, 158 | } 159 | v.Errors = append(v.Errors, err) 160 | 161 | // Also return it in the result. 162 | return &ValidationResult{ 163 | Ok: false, 164 | Error: err, 165 | } 166 | } 167 | 168 | // Apply a group of validators to a field, in order, and return the 169 | // ValidationResult from the first one that fails, or the last one that 170 | // succeeds. 171 | func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResult { 172 | var result *ValidationResult 173 | for _, check := range checks { 174 | result = v.apply(check, obj) 175 | if !result.Ok { 176 | return result 177 | } 178 | } 179 | return result 180 | } 181 | 182 | // Revel Filter function to be hooked into the filter chain. 183 | func ValidationFilter(c *Controller, fc []Filter) { 184 | errors, err := restoreValidationErrors(c.Request.Request) 185 | c.Validation = &Validation{ 186 | Errors: errors, 187 | keep: false, 188 | } 189 | hasCookie := (err != http.ErrNoCookie) 190 | 191 | fc[0](c, fc[1:]) 192 | 193 | // Add Validation errors to RenderArgs. 194 | c.RenderArgs["errors"] = c.Validation.ErrorMap() 195 | 196 | // Store the Validation errors 197 | var errorsValue string 198 | if c.Validation.keep { 199 | for _, error := range c.Validation.Errors { 200 | if error.Message != "" { 201 | errorsValue += "\x00" + error.Key + ":" + error.Message + "\x00" 202 | } 203 | } 204 | } 205 | 206 | // When there are errors from Validation and Keep() has been called, store the 207 | // values in a cookie. If there previously was a cookie but no errors, remove 208 | // the cookie. 209 | if errorsValue != "" { 210 | c.SetCookie(&http.Cookie{ 211 | Name: CookiePrefix + "_ERRORS", 212 | Value: url.QueryEscape(errorsValue), 213 | Domain: CookieDomain, 214 | Path: "/", 215 | HttpOnly: true, 216 | Secure: CookieSecure, 217 | }) 218 | } else if hasCookie { 219 | c.SetCookie(&http.Cookie{ 220 | Name: CookiePrefix + "_ERRORS", 221 | MaxAge: -1, 222 | Domain: CookieDomain, 223 | Path: "/", 224 | HttpOnly: true, 225 | Secure: CookieSecure, 226 | }) 227 | } 228 | } 229 | 230 | // Restore Validation.Errors from a request. 231 | func restoreValidationErrors(req *http.Request) ([]*ValidationError, error) { 232 | var ( 233 | err error 234 | cookie *http.Cookie 235 | errors = make([]*ValidationError, 0, 5) 236 | ) 237 | if cookie, err = req.Cookie(CookiePrefix + "_ERRORS"); err == nil { 238 | ParseKeyValueCookie(cookie.Value, func(key, val string) { 239 | errors = append(errors, &ValidationError{ 240 | Key: key, 241 | Message: val, 242 | }) 243 | }) 244 | } 245 | return errors, err 246 | } 247 | 248 | // Register default validation keys for all calls to Controller.Validation.Func(). 249 | // Map from (package).func => (line => name of first arg to Validation func) 250 | // E.g. "myapp/controllers.helper" or "myapp/controllers.(*Application).Action" 251 | // This is set on initialization in the generated main.go file. 252 | var DefaultValidationKeys map[string]map[int]string 253 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package revel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "reflect" 14 | "regexp" 15 | "strings" 16 | 17 | "github.com/revel/config" 18 | ) 19 | 20 | const ( 21 | DefaultFileContentType = "application/octet-stream" 22 | ) 23 | 24 | var ( 25 | cookieKeyValueParser = regexp.MustCompile("\x00([^:]*):([^\x00]*)\x00") 26 | hdrForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") 27 | hdrRealIP = http.CanonicalHeaderKey("X-Real-Ip") 28 | 29 | mimeConfig *config.Context 30 | ) 31 | 32 | // Add some more methods to the default Template. 33 | type ExecutableTemplate interface { 34 | Execute(io.Writer, interface{}) error 35 | } 36 | 37 | // Execute a template and returns the result as a string. 38 | func ExecuteTemplate(tmpl ExecutableTemplate, data interface{}) string { 39 | var b bytes.Buffer 40 | tmpl.Execute(&b, data) 41 | return b.String() 42 | } 43 | 44 | // Reads the lines of the given file. Panics in the case of error. 45 | func MustReadLines(filename string) []string { 46 | r, err := ReadLines(filename) 47 | if err != nil { 48 | panic(err) 49 | } 50 | return r 51 | } 52 | 53 | // Reads the lines of the given file. Panics in the case of error. 54 | func ReadLines(filename string) ([]string, error) { 55 | bytes, err := ioutil.ReadFile(filename) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return strings.Split(string(bytes), "\n"), nil 60 | } 61 | 62 | func ContainsString(list []string, target string) bool { 63 | for _, el := range list { 64 | if el == target { 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | 71 | // Return the reflect.Method, given a Receiver type and Func value. 72 | func FindMethod(recvType reflect.Type, funcVal reflect.Value) *reflect.Method { 73 | // It is not possible to get the name of the method from the Func. 74 | // Instead, compare it to each method of the Controller. 75 | for i := 0; i < recvType.NumMethod(); i++ { 76 | method := recvType.Method(i) 77 | if method.Func.Pointer() == funcVal.Pointer() { 78 | return &method 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | // Takes the raw (escaped) cookie value and parses out key values. 85 | func ParseKeyValueCookie(val string, cb func(key, val string)) { 86 | val, _ = url.QueryUnescape(val) 87 | if matches := cookieKeyValueParser.FindAllStringSubmatch(val, -1); matches != nil { 88 | for _, match := range matches { 89 | cb(match[1], match[2]) 90 | } 91 | } 92 | } 93 | 94 | // Load mime-types.conf on init. 95 | func LoadMimeConfig() { 96 | var err error 97 | mimeConfig, err = config.LoadContext("mime-types.conf", ConfPaths) 98 | if err != nil { 99 | ERROR.Fatalln("Failed to load mime type config:", err) 100 | } 101 | } 102 | 103 | // Returns a MIME content type based on the filename's extension. 104 | // If no appropriate one is found, returns "application/octet-stream" by default. 105 | // Additionally, specifies the charset as UTF-8 for text/* types. 106 | func ContentTypeByFilename(filename string) string { 107 | dot := strings.LastIndex(filename, ".") 108 | if dot == -1 || dot+1 >= len(filename) { 109 | return DefaultFileContentType 110 | } 111 | 112 | extension := filename[dot+1:] 113 | contentType := mimeConfig.StringDefault(extension, "") 114 | if contentType == "" { 115 | return DefaultFileContentType 116 | } 117 | 118 | if strings.HasPrefix(contentType, "text/") { 119 | return contentType + "; charset=utf-8" 120 | } 121 | 122 | return contentType 123 | } 124 | 125 | // DirExists returns true if the given path exists and is a directory. 126 | func DirExists(filename string) bool { 127 | fileInfo, err := os.Stat(filename) 128 | return err == nil && fileInfo.IsDir() 129 | } 130 | 131 | func FirstNonEmpty(strs ...string) string { 132 | for _, str := range strs { 133 | if len(str) > 0 { 134 | return str 135 | } 136 | } 137 | return "" 138 | } 139 | 140 | // Equal is a helper for comparing value equality, following these rules: 141 | // - Values with equivalent types are compared with reflect.DeepEqual 142 | // - int, uint, and float values are compared without regard to the type width. 143 | // for example, Equal(int32(5), int64(5)) == true 144 | // - strings and byte slices are converted to strings before comparison. 145 | // - else, return false. 146 | func Equal(a, b interface{}) bool { 147 | if reflect.TypeOf(a) == reflect.TypeOf(b) { 148 | return reflect.DeepEqual(a, b) 149 | } 150 | switch a.(type) { 151 | case int, int8, int16, int32, int64: 152 | switch b.(type) { 153 | case int, int8, int16, int32, int64: 154 | return reflect.ValueOf(a).Int() == reflect.ValueOf(b).Int() 155 | } 156 | case uint, uint8, uint16, uint32, uint64: 157 | switch b.(type) { 158 | case uint, uint8, uint16, uint32, uint64: 159 | return reflect.ValueOf(a).Uint() == reflect.ValueOf(b).Uint() 160 | } 161 | case float32, float64: 162 | switch b.(type) { 163 | case float32, float64: 164 | return reflect.ValueOf(a).Float() == reflect.ValueOf(b).Float() 165 | } 166 | case string: 167 | switch b.(type) { 168 | case []byte: 169 | return a.(string) == string(b.([]byte)) 170 | } 171 | case []byte: 172 | switch b.(type) { 173 | case string: 174 | return b.(string) == string(a.([]byte)) 175 | } 176 | } 177 | return false 178 | } 179 | 180 | // ClientIP method returns client IP address from HTTP request. 181 | // 182 | // Note: Set property "app.behind.proxy" to true only if Revel is running 183 | // behind proxy like nginx, haproxy, apache, etc. Otherwise 184 | // you may get inaccurate Client IP address. Revel parses the 185 | // IP address in the order of X-Forwarded-For, X-Real-IP. 186 | // 187 | // By default revel will get http.Request's RemoteAddr 188 | func ClientIP(r *http.Request) string { 189 | if Config.BoolDefault("app.behind.proxy", false) { 190 | // Header X-Forwarded-For 191 | if fwdFor := strings.TrimSpace(r.Header.Get(hdrForwardedFor)); fwdFor != "" { 192 | index := strings.Index(fwdFor, ",") 193 | if index == -1 { 194 | return fwdFor 195 | } 196 | return fwdFor[:index] 197 | } 198 | 199 | // Header X-Real-Ip 200 | if realIP := strings.TrimSpace(r.Header.Get(hdrRealIP)); realIP != "" { 201 | return realIP 202 | } 203 | } 204 | 205 | if remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { 206 | return remoteAddr 207 | } 208 | 209 | return "" 210 | } 211 | 212 | // Walk method extends filepath.Walk to also follow symlinks. 213 | // Always returns the path of the file or directory. 214 | func Walk(root string, walkFn filepath.WalkFunc) error { 215 | return fsWalk(root, root, walkFn) 216 | } 217 | 218 | // createDir method creates nested directories if not exists 219 | func createDir(path string) error { 220 | if _, err := os.Stat(path); err != nil { 221 | if os.IsNotExist(err) { 222 | if err = os.MkdirAll(path, 0755); err != nil { 223 | return fmt.Errorf("Failed to create directory '%v': %v", path, err) 224 | } 225 | } else { 226 | return fmt.Errorf("Failed to create directory '%v': %v", path, err) 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error { 233 | fsWalkFunc := func(path string, info os.FileInfo, err error) error { 234 | if err != nil { 235 | return err 236 | } 237 | 238 | var name string 239 | name, err = filepath.Rel(fname, path) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | path = filepath.Join(linkName, name) 245 | 246 | if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { 247 | var symlinkPath string 248 | symlinkPath, err = filepath.EvalSymlinks(path) 249 | if err != nil { 250 | return err 251 | } 252 | 253 | // https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392 254 | info, err = os.Lstat(symlinkPath) 255 | if err != nil { 256 | return walkFn(path, info, err) 257 | } 258 | 259 | if info.IsDir() { 260 | return fsWalk(symlinkPath, path, walkFn) 261 | } 262 | } 263 | 264 | return walkFn(path, info, err) 265 | } 266 | 267 | return filepath.Walk(fname, fsWalkFunc) 268 | } 269 | 270 | func init() { 271 | OnAppStart(LoadMimeConfig) 272 | } 273 | -------------------------------------------------------------------------------- /testing/testsuite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. 2 | // Revel Framework source code and usage is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package testing 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/revel/revel" 20 | ) 21 | 22 | func TestMisc(t *testing.T) { 23 | testSuite := createNewTestSuite(t) 24 | 25 | // test Host value 26 | if !strings.EqualFold("127.0.0.1:9001", testSuite.Host()) { 27 | t.Error("Incorrect Host value found.") 28 | } 29 | 30 | // test BaseUrl 31 | if !strings.EqualFold("http://127.0.0.1:9001", testSuite.BaseUrl()) { 32 | t.Error("Incorrect BaseUrl http value found.") 33 | } 34 | revel.HttpSsl = true 35 | if !strings.EqualFold("https://127.0.0.1:9001", testSuite.BaseUrl()) { 36 | t.Error("Incorrect BaseUrl https value found.") 37 | } 38 | revel.HttpSsl = false 39 | 40 | // test WebSocketUrl 41 | if !strings.EqualFold("ws://127.0.0.1:9001", testSuite.WebSocketUrl()) { 42 | t.Error("Incorrect WebSocketUrl value found.") 43 | } 44 | 45 | testSuite.AssertNotEqual("Yes", "No") 46 | testSuite.Assert(true) 47 | } 48 | 49 | func TestGet(t *testing.T) { 50 | ts := createTestServer(testHandle) 51 | defer ts.Close() 52 | 53 | testSuite := createNewTestSuite(t) 54 | 55 | testSuite.Get("/") 56 | testSuite.AssertOk() 57 | testSuite.AssertContains("this is testcase homepage") 58 | testSuite.AssertNotContains("not exists") 59 | } 60 | 61 | func TestGetNotFound(t *testing.T) { 62 | ts := createTestServer(testHandle) 63 | defer ts.Close() 64 | 65 | testSuite := createNewTestSuite(t) 66 | 67 | testSuite.Get("/notfound") 68 | testSuite.AssertNotFound() 69 | // testSuite.AssertContains("this is testcase homepage") 70 | // testSuite.AssertNotContains("not exists") 71 | } 72 | 73 | func TestGetCustom(t *testing.T) { 74 | testSuite := createNewTestSuite(t) 75 | testSuite.GetCustom("http://httpbin.org/get").Send() 76 | 77 | testSuite.AssertOk() 78 | testSuite.AssertContentType("application/json") 79 | testSuite.AssertHeader("Server", "nginx") 80 | testSuite.AssertContains("httpbin.org") 81 | testSuite.AssertContainsRegex("gzip|deflate") 82 | } 83 | 84 | func TestDelete(t *testing.T) { 85 | ts := createTestServer(testHandle) 86 | defer ts.Close() 87 | 88 | testSuite := createNewTestSuite(t) 89 | 90 | testSuite.Delete("/purchases/10001") 91 | testSuite.AssertOk() 92 | } 93 | 94 | func TestPut(t *testing.T) { 95 | ts := createTestServer(testHandle) 96 | defer ts.Close() 97 | 98 | testSuite := createNewTestSuite(t) 99 | 100 | testSuite.Put("/purchases/10002", 101 | "application/json", 102 | bytes.NewReader([]byte(`{"sku":"163645GHT", "desc":"This is test product"}`)), 103 | ) 104 | testSuite.AssertStatus(http.StatusNoContent) 105 | } 106 | 107 | func TestPutForm(t *testing.T) { 108 | ts := createTestServer(testHandle) 109 | defer ts.Close() 110 | 111 | testSuite := createNewTestSuite(t) 112 | 113 | data := url.Values{} 114 | data.Add("name", "beacon1name") 115 | data.Add("value", "beacon1value") 116 | 117 | testSuite.PutForm("/send", data) 118 | testSuite.AssertStatus(http.StatusNoContent) 119 | } 120 | 121 | func TestPatch(t *testing.T) { 122 | ts := createTestServer(testHandle) 123 | defer ts.Close() 124 | 125 | testSuite := createNewTestSuite(t) 126 | 127 | testSuite.Patch("/purchases/10003", 128 | "application/json", 129 | bytes.NewReader([]byte(`{"desc": "This is test patch for product"}`)), 130 | ) 131 | testSuite.AssertStatus(http.StatusNoContent) 132 | } 133 | 134 | func TestPost(t *testing.T) { 135 | ts := createTestServer(testHandle) 136 | defer ts.Close() 137 | 138 | testSuite := createNewTestSuite(t) 139 | fmt.Println(testSuite.Session.Cookie().Name) 140 | 141 | testSuite.Post("/login", 142 | "application/json", 143 | bytes.NewReader([]byte(`{"username":"testuser", "password":"testpass"}`)), 144 | ) 145 | testSuite.AssertOk() 146 | testSuite.AssertContains("login successful") 147 | } 148 | 149 | func TestPostForm(t *testing.T) { 150 | ts := createTestServer(testHandle) 151 | defer ts.Close() 152 | 153 | testSuite := createNewTestSuite(t) 154 | 155 | data := url.Values{} 156 | data.Add("username", "testuser") 157 | data.Add("password", "testpassword") 158 | 159 | testSuite.PostForm("/login", data) 160 | testSuite.AssertOk() 161 | testSuite.AssertContains("login successful") 162 | } 163 | 164 | func TestPostFileUpload(t *testing.T) { 165 | ts := createTestServer(testHandle) 166 | defer ts.Close() 167 | 168 | testSuite := createNewTestSuite(t) 169 | 170 | params := url.Values{} 171 | params.Add("first_name", "Jeevanandam") 172 | params.Add("last_name", "M.") 173 | 174 | currentDir, _ := os.Getwd() 175 | basePath := filepath.Dir(currentDir) 176 | 177 | filePaths := url.Values{} 178 | filePaths.Add("revel_file", filepath.Join(basePath, "revel.go")) 179 | filePaths.Add("server_file", filepath.Join(basePath, "server.go")) 180 | filePaths.Add("readme_file", filepath.Join(basePath, "README.md")) 181 | 182 | testSuite.PostFile("/upload", params, filePaths) 183 | 184 | testSuite.AssertOk() 185 | testSuite.AssertContains("File: revel.go") 186 | testSuite.AssertContains("File: server.go") 187 | testSuite.AssertNotContains("File: not_exists.go") 188 | testSuite.AssertEqual("text/plain; charset=utf-8", testSuite.Response.Header.Get("Content-Type")) 189 | 190 | } 191 | 192 | func createNewTestSuite(t *testing.T) *TestSuite { 193 | suite := NewTestSuite() 194 | 195 | if suite.Client == nil || suite.Session == nil { 196 | t.Error("Unable to create a testsuite") 197 | } 198 | 199 | return &suite 200 | } 201 | 202 | func testHandle(w http.ResponseWriter, r *http.Request) { 203 | if r.Method == "GET" { 204 | if r.URL.Path == "/" { 205 | _, _ = w.Write([]byte(`this is testcase homepage`)) 206 | return 207 | } 208 | } 209 | 210 | if r.Method == "POST" { 211 | if r.URL.Path == "/login" { 212 | http.SetCookie(w, &http.Cookie{ 213 | Name: "_SESSION", 214 | Value: "This is simple session value", 215 | Path: "/", 216 | HttpOnly: true, 217 | Secure: false, 218 | Expires: time.Now().Add(time.Minute * 5).UTC(), 219 | }) 220 | 221 | w.Header().Set("Content-Type", "application/json") 222 | _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) 223 | return 224 | } 225 | 226 | handleFileUpload(w, r) 227 | return 228 | } 229 | 230 | if r.Method == "DELETE" { 231 | if r.URL.Path == "/purchases/10001" { 232 | w.WriteHeader(http.StatusOK) 233 | return 234 | } 235 | } 236 | 237 | if r.Method == "PUT" { 238 | if r.URL.Path == "/purchases/10002" { 239 | w.WriteHeader(http.StatusNoContent) 240 | return 241 | } 242 | 243 | if r.URL.Path == "/send" { 244 | w.WriteHeader(http.StatusNoContent) 245 | return 246 | } 247 | } 248 | 249 | if r.Method == "PATCH" { 250 | if r.URL.Path == "/purchases/10003" { 251 | w.WriteHeader(http.StatusNoContent) 252 | return 253 | } 254 | } 255 | 256 | w.WriteHeader(http.StatusNotFound) 257 | } 258 | 259 | func handleFileUpload(w http.ResponseWriter, r *http.Request) { 260 | if r.URL.Path == "/upload" { 261 | _ = r.ParseMultipartForm(10e6) 262 | var buf bytes.Buffer 263 | for _, fhdrs := range r.MultipartForm.File { 264 | for _, hdr := range fhdrs { 265 | dotPos := strings.LastIndex(hdr.Filename, ".") 266 | fname := fmt.Sprintf("%s-%v%s", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:]) 267 | _, _ = buf.WriteString(fmt.Sprintf( 268 | "Firstname: %v\nLastname: %v\nFile: %v\nHeader: %v\nUploaded as: %v\n", 269 | r.FormValue("first_name"), 270 | r.FormValue("last_name"), 271 | hdr.Filename, 272 | hdr.Header, 273 | fname)) 274 | } 275 | } 276 | 277 | _, _ = w.Write(buf.Bytes()) 278 | 279 | return 280 | } 281 | } 282 | 283 | func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server { 284 | testServer := httptest.NewServer(http.HandlerFunc(fn)) 285 | revel.Server.Addr = testServer.URL[7:] 286 | return testServer 287 | } 288 | 289 | func init() { 290 | if revel.Server == nil { 291 | revel.Server = &http.Server{ 292 | Addr: ":9001", 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------