Add API framework Revel source files, as the basis for api framework.
Signed-off-by: Trevor Tao <trevor.tao@arm.com>
Change-Id: I8c60569238a66538e86a2cc98a0d6473df2f8e72
--- /dev/null
+{
+ "GOLANG": {
+ "ABC":[15, 25, 50, 70],
+ "BLOCK_NESTING":[5, 6, 7, 8],
+ "CYCLO":[20, 30, 45, 60],
+ "TOO_MANY_IVARS": [15, 18, 20, 25],
+ "TOO_MANY_FUNCTIONS": [20, 30, 40, 50],
+ "TOTAL_COMPLEXITY": [150, 250, 400, 500],
+ "LOC": [50, 75, 90, 120]
+ }
+}
\ No newline at end of file
--- /dev/null
+language: go
+
+go:
+ - "1.8.x"
+ - "1.9.x"
+ - "1.10.x"
+ - "1.11.x"
+ - "tip"
+
+os:
+ - linux
+ - osx
+ - windows
+
+sudo: false
+
+branches:
+ only:
+ - master
+ - develop
+
+services:
+ # github.com/revel/revel/cache
+ - memcache
+ - redis-server
+
+before_install:
+ # TRAVIS_OS_NAME - linux and osx
+ - echo $TRAVIS_OS_NAME
+ - echo $PATH
+ - |
+ if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+ brew update && brew install memcached redis && brew services start redis && brew services start memcached
+ fi
+ - |
+ if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then
+ redis-server --daemonize yes
+ redis-cli info
+ else
+ # redis-server.exe
+ # redis-cli.exe info
+ echo $PATH
+ fi
+
+install:
+ # Setting environments variables
+ - export PATH=$PATH:$HOME/gopath/bin
+ - export REVEL_BRANCH="develop"
+ - 'if [[ "$TRAVIS_BRANCH" == "master" ]]; then export REVEL_BRANCH="master"; fi'
+ - 'echo "Travis branch: $TRAVIS_BRANCH, Revel dependency branch: $REVEL_BRANCH"'
+ - git clone -b $REVEL_BRANCH git://github.com/revel/modules ../modules/
+ - git clone -b $REVEL_BRANCH git://github.com/revel/cmd ../cmd/
+ - git clone -b $REVEL_BRANCH git://github.com/revel/config ../config/
+ - git clone -b $REVEL_BRANCH git://github.com/revel/cron ../cron/
+ - git clone -b $REVEL_BRANCH git://github.com/revel/examples ../examples/
+ - go get -t -v github.com/revel/revel/...
+ - go get -t -v github.com/revel/cmd/revel
+
+script:
+ - go test -v github.com/revel/revel/...
+
+matrix:
+ allow_failures:
+ - go: tip
+ - os: windows
+ - go: 1.6
+ os: osx
--- /dev/null
+# TODO Revel Framework Authors Information
--- /dev/null
+# CHANGELOG
+
+## v0.19.0
+# Release 0.19.0
+
+# Maintenance Release
+
+This release is focused on improving the security and resolving some issues.
+
+**There are no breaking changes from version 0.18**
+
+[[revel/cmd](https://github.com/revel/cmd)]
+* Improved vendor folder detection revel/cmd#117
+* Added ordering of controllers so order remains consistent in main.go revel/cmd#112
+* Generate same value of `AppVersion` regardless of where Revel is run revel/cmd#108
+* Added referrer policy security header revel/cmd#114
+
+[[revel/modules](https://github.com/revel/modules)]
+* Added directory representation to static module revel/modules#46
+* Gorp enhancements (added abstraction layer for transactions and database connection so both can be used), Added security fix for CSRF module revel/modules#68
+* Added authorization configuration options to job page revel/modules#44
+
+[[revel/examples](https://github.com/revel/examples)]
+* General improvements and examples added revel/examples#39 revel/examples#40
+
+## v0.18
+# Release 0.18
+
+## Upgrade path
+The main breaking change is the removal of `http.Request` from the `revel.Request` object.
+Everything else should just work....
+
+## New items
+* Server Engine revel/revel#998
+The server engine implementation is described in the [docs](http://revel.github.io/manual/server-engine.html)
+* Allow binding to a structured map. revel/revel#998
+Have a structure inside a map object which will be realized properly from params
+* Gorm module revel/modules/#51
+Added transaction controller
+* Gorp module revel/modules/#52
+* Autorun on startup in develop mode revel/cmd#95
+Start the application without doing a request first using revel run ....
+* Logger update revel/revel#1213
+Configurable logger and added context logging on controller via controller.Log
+* Before after finally panic controller method detection revel/revel#1211
+Controller methods will be automatically detected and called - similar to interceptors but without the extra code
+* Float validation revel/revel#1209
+Added validation for floats
+* Timeago template function revel/revel#1207
+Added timeago function to Revel template functions
+* Authorization to jobs module revel/module#44
+Added ability to specify authorization to access the jobs module routes
+* Add MessageKey, ErrorKey methods to ValidationResult object revel/revel#1215
+This allows the message translator to translate the keys added. So model objects can send out validation codes
+* Vendor friendlier - Revel recognizes and uses `deps` (to checkout go libraries) if a vendor folder exists in the project root.
+* Updated examples to use Gorp modules and new loggers
+
+
+### Breaking Changes
+
+* `http.Request` is no longer contained in `revel.Request` revel.Request remains functionally the same but
+you cannot extract the `http.Request` from it. You can get the `http.Request` from `revel.Controller.Request.In.GetRaw().(*http.Request)`
+* `http.Response.Out` Is not the http.Response and is deprecated, you can get the output writer by doing `http.Response.GetWriter()`. You can get the `http.Response` from revel.Controller.Response.Out.Server.GetRaw().(*http.Response)`
+
+* `Websocket` changes. `revel.ServerWebsocket` is the new type of object you need to declare for controllers
+which should need to attach to websockets. Implementation of these objects have been simplified
+
+Old
+```
+
+func (c WebSocket) RoomSocket(user string, ws *websocket.Conn) revel.Result {
+ // Join the room.
+ subscription := chatroom.Subscribe()
+ defer subscription.Cancel()
+
+ chatroom.Join(user)
+ defer chatroom.Leave(user)
+
+ // Send down the archive.
+ for _, event := range subscription.Archive {
+ if websocket.JSON.Send(ws, &event) != nil {
+ // They disconnected
+ return nil
+ }
+ }
+
+ // In order to select between websocket messages and subscription events, we
+ // need to stuff websocket events into a channel.
+ newMessages := make(chan string)
+ go func() {
+ var msg string
+ for {
+ err := websocket.Message.Receive(ws, &msg)
+ if err != nil {
+ close(newMessages)
+ return
+ }
+ newMessages <- msg
+ }
+ }()
+```
+New
+```
+func (c WebSocket) RoomSocket(user string, ws revel.ServerWebSocket) revel.Result {
+ // Join the room.
+ subscription := chatroom.Subscribe()
+ defer subscription.Cancel()
+
+ chatroom.Join(user)
+ defer chatroom.Leave(user)
+
+ // Send down the archive.
+ for _, event := range subscription.Archive {
+ if ws.MessageSendJSON(&event) != nil {
+ // They disconnected
+ return nil
+ }
+ }
+
+ // In order to select between websocket messages and subscription events, we
+ // need to stuff websocket events into a channel.
+ newMessages := make(chan string)
+ go func() {
+ var msg string
+ for {
+ err := ws.MessageReceiveJSON(&msg)
+ if err != nil {
+ close(newMessages)
+ return
+ }
+ newMessages <- msg
+ }
+ }()
+```
+* GORM module has been refactored into modules/orm/gorm
+
+
+### Deprecated methods
+* `revel.Request.FormValue()` Is deprecated, you should use methods in the controller.Params to access this data
+* `revel.Request.PostFormValue()` Is deprecated, you should use methods in the controller.Params.Form to access this data
+* `revel.Request.ParseForm()` Is deprecated - not needed
+* `revel.Request.ParseMultipartForm()` Is deprecated - not needed
+* `revel.Request.Form` Is deprecated, you should use the controller.Params.Form to access this data
+* `revel.Request.MultipartForm` Is deprecated, you should use the controller.Params.Form to access this data
+* `revel.TRACE`, `revel.INFO` `revel.WARN` `revel.ERROR` are deprecated. Use new application logger `revel.AppLog` and the controller logger `controller.Log`. See [logging](http://revel.github.io/manual/logging.html) for more details.
+
+### Features
+
+* Pluggable server engine support. You can now implement **your own server engine**. This means if you need to listen to more then 1 IP address or port you can implement a custom server engine to do this. By default Revel uses GO http server, but also available is fasthttp server in the revel/modules repository. See the docs for more information on how to implement your own engine.
+
+### Enhancements
+* Controller instances are cached for reuse. This speeds up the request response time and prevents unnecessary garbage collection cycles.
+
+### Bug fixes
+
+
+
+
+## v0.17
+
+[[revel/revel](https://github.com/revel/revel)]
+
+* add-validation
+* i18-lang-by-param
+* Added namespace to routes, controllers
+* Added go 1.6 to testing
+* Adds the ability to set the language by a url parameter. The route file will need to specify the parameter so that it will be picked up
+* Changed url validation logic to regex
+* Added new validation mehtods (IPAddr,MacAddr,Domain,URL,PureText)
+
+[[revel/cmd](https://github.com/revel/cmd)]
+
+* no changes
+
+[[revel/config](https://github.com/revel/config)]
+
+* no changes
+
+[[revel/modules](https://github.com/revel/modules)]
+
+* Added Gorm module
+
+[[revel/cron](https://github.com/revel/cron)]
+
+* Updated cron task manager
+* Added ability to run a specific job, reschedules job if cron is running.
+
+[[revel/examples](https://github.com/revel/examples)]
+
+* Gorm module (Example)
+
+# v0.16.0
+
+Deprecating support for golang versions prior to 1.6
+### Breaking Changes
+
+* `CurrentLocaleRenderArg` to `CurrentLocaleViewArg` for consistency
+* JSON requests are now parsed by Revel, if the content type is `text/json` or `application/json`. The raw data is available in `Revel.Controller.Params.JSON`. But you can also use the automatic controller operation to load the data like you would any structure or map. See [here](http://revel.github.io/manual/parameters.html) for more details
+
+### Features
+
+* Modular Template Engine #1170
+* Pongo2 engine driver added revel/modules#39
+* Ace engine driver added revel/modules#40
+* Added i18n template support #746
+
+### Enhancements
+
+* JSON request binding #1161
+* revel.SetSecretKey function added #1127
+* ResolveFormat now looks at the extension as well (this sets the content type) #936
+* Updated command to run tests using the configuration revel/cmd#61
+
+### Bug fixes
+
+* Updated documentation typos revel/modules#37
+* Updated order of parameter map assignment #1155
+* Updated cookie lifetime for firefox #1174
+* Added test path for modules, so modules will run tests as well #1162
+* Fixed go profiler module revel/modules#20
+
+
+# v0.15.0
+@shawncatz released this on 2017-05-11
+
+Deprecating support for golang versions prior to 1.7
+
+### Breaking Changes
+
+* None
+
+### Features
+
+* None
+
+### Enhancements
+
+* Update and improve docs revel/examples#17 revel/cmd#85
+
+### Bug fixes
+
+* Prevent XSS revel/revel#1153
+* Improve error checking for go version detection revel/cmd#86
+
+# v0.14.0
+@notzippy released this on 2017-03-24
+
+## Changes since v0.13.0
+
+#### Breaking Changes
+- `revel/revel`:
+ - change RenderArgs to ViewArgs PR #1135
+ - change RenderJson to RenderJSON PR #1057
+ - change RenderHtml to RenderHTML PR #1057
+ - change RenderXml to RenderXML PR #1057
+
+#### Features
+- `revel/revel`:
+
+#### Enhancements
+- `revel/revel`:
+
+
+#### Bug Fixes
+- `revel/revel`:
+
+
+# v0.13.1
+@jeevatkm released this on 2016-06-07
+
+**Bug fix:**
+- Windows path fix #1064
+
+
+# v0.13.0
+@jeevatkm released this on 2016-06-06
+
+## Changes since v0.12.0
+
+#### Breaking Changes
+- `revel/revel`:
+ - Application Config name changed from `watcher.*` to `watch.*` PR #992, PR #991
+
+#### Features
+- `revel/revel`:
+ - Request access log PR #1059, PR #913, #1055
+ - Messages loaded from modules too PR #828
+- `revel/cmd`:
+ - Added `revel version` command emits the revel version and go version revel/cmd#19
+
+#### Enhancements
+- `revel/revel`:
+ - Creates log directory if missing PR #1039
+ - Added `application/javascript` to accepted headers PR #1022
+ - You can change `Server.Addr` value via hook function PR #999
+ - Improved deflate/gzip compressor PR #995
+ - Consistent config name `watch.*` PR #992, PR #991
+ - Defaults to HttpOnly and always secure cookies for non-dev mode #942, PR #943
+ - Configurable server Read and Write Timeout via app config #936, PR #940
+ - `OnAppStart` hook now supports order param too PR #935
+ - Added `PutForm` and `PutFormCustom` helper method in `testing.TestSuite` #898
+ - Validator supports UTF-8 string too PR #891, #841
+ - Added `InitServer` method that returns `http.HandlerFunc` PR #879
+ - Symlink aware processing Views, Messages and Watch mode PR #867, #673
+ - Added i18n settings support unknown format PR #852
+ - i18n: Make Message Translation pluggable PR #768
+ - jQuery `min-2.2.4` & Bootstrap `min-3.3.6` version updated in `skeleton/public` #1063
+- `revel/cmd`:
+ - Revel identifies current `GOPATH` and performs `new` command; relative to directory revel/revel#1004
+ - Installs package dependencies during a build PR revel/cmd#43
+ - Non-200 response of test case request will correctly result into error PR revel/cmd#38
+ - Websockets SSL support in `dev` mode PR revel/cmd#32
+ - Won't yell about non-existent directory while cleaning PR revel/cmd#31, #908
+ - [x] non-fatal errors when building #908
+ - Improved warnings about route generation PR revel/cmd#25
+ - Command is Symlink aware PR revel/cmd#20
+ - `revel package` & `revel build` now supports environment mode PR revel/cmd#14
+ - `revel clean` now cleans generated routes too PR revel/cmd#6
+- `revel/config`:
+ - Upstream `robfig/config` refresh and import path updated from `github.com/revel/revel/config` to `github.com/revel/config`, PR #868
+ - Config loading order and external configuration to override application configuration revel/config#4 [commit](https://github.com/revel/revel/commit/f3a422c228994978ae0a5dd837afa97248b26b41)
+ - Application config error will produce insight on error PR revel/config#3 [commit](https://github.com/revel/config/commit/85a123061070899a82f59c5ef6187e8fb4457f64)
+- `revel/modules`:
+ - Testrunner enhancements
+ - Minor improvement on testrunner module PR #820, #895
+ - Add Test Runner panels per test group PR revel/modules#12
+- `revel/revel.github.io`:
+ - Update `index.md` and homepage (change how samples repo is installed) PR [#85](https://github.com/revel/revel.github.io/pull/85)
+ - Couple of UI improvements PR [#93](https://github.com/revel/revel.github.io/pull/93)
+ - Updated techempower benchmarks Round 11 [URL](http://www.techempower.com/benchmarks/#section=data-r11)
+ - Docs updated for v0.13 release
+- Cross-Platform Support
+ - Slashes should be normalized in paths #260, PR #1028, PR #928
+
+#### Bug Fixes
+- `revel/revel`:
+ - Binder: Multipart `io.Reader` parameters needs to be closed #756
+ - Default Date & Time Format correct in skeleton PR #1062, #878
+ - Addressed with alternative for `json: unsupported type: <-chan struct {}` on Go 1.6 revel/revel#1037
+ - Addressed one edge case, invalid Accept-Encoding header causes panic revel/revel#914
+
+
+# v0.11.3
+@brendensoares released this on 2015-01-04
+
+This is a minor release to address a critical bug (#824) in v0.11.2.
+
+Everybody is strongly encouraged to rebuild their projects with the latest version of Revel. To do it, execute the commands:
+
+``` sh
+$ go get -u github.com/revel/cmd/revel
+
+$ revel build github.com/myusername/myproject /path/to/destination/folder
+```
+
+
+# v0.11.2
+on 2014-11-23
+
+This is a minor release to address a critical bug in v0.11.0.
+
+Everybody is strongly encouraged to rebuild their projects with the latest version of Revel. To do it, execute the commands:
+
+``` sh
+$ go get -u github.com/revel/cmd/revel
+
+$ revel build github.com/myusername/myproject /path/to/destination/folder
+```
+
+
+# v0.11.1
+@pushrax released this on 2014-10-27
+
+This is a minor release to address a compilation error in v0.11.0.
+
+
+# v0.12.0
+@brendensoares released this on 2015-03-25
+
+Changes since v0.11.3:
+
+## Breaking Changes
+1. Add import path to new `testing` sub-package for all Revel tests. For example:
+
+``` go
+package tests
+
+import "github.com/revel/revel/testing"
+
+type AppTest struct {
+ testing.TestSuite
+}
+```
+1. We've relocated modules to a dedicated repo. Make sure you update your `conf/app.conf`. For example, change:
+
+``` ini
+module.static=github.com/revel/revel/modules/static
+module.testrunner = github.com/revel/revel/modules/testrunner
+```
+
+to the new paths:
+
+``` ini
+module.static=github.com/revel/modules/static
+module.testrunner = github.com/revel/modules/testrunner
+```
+
+## [ROADMAP] Focus: Improve Internal Organization
+
+The majority of our effort here is increasing the modularity of the code within Revel so that further development can be done more productively while keeping documentation up to date.
+- `revel/revel.github.io`
+ - [x] Improve docs #[43](https://github.com/revel/revel.github.io/pull/43)
+- `revel/revel`:
+ - [x] Move the `revel/revel/harness` to the `revel/cmd` repo since it's only used during build time. #[714](https://github.com/revel/revel/issues/714)
+ - [x] Move `revel/revel/modules` to the `revel/modules` repo #[785](https://github.com/revel/revel/issues/785)
+ - [x] Move `revel/revel/samples` to the `revel/samples` repo #[784](https://github.com/revel/revel/issues/784)
+ - [x] `testing` TestSuite #[737](https://github.com/revel/revel/issues/737) #[810](https://github.com/revel/revel/issues/810)
+ - [x] Feature/sane http timeout defaults #[837](https://github.com/revel/revel/issues/837) PR#[843](https://github.com/revel/revel/issues/843) Bug Fix PR#[860](https://github.com/revel/revel/issues/860)
+ - [x] Eagerly load templates in dev mode #[353](https://github.com/revel/revel/issues/353) PR#[844](https://github.com/revel/revel/pull/844)
+ - [x] Add an option to trim whitespace from rendered HTML #[800](https://github.com/revel/revel/issues/800)
+ - [x] Remove built-in mailer in favor of 3rd party package #[783](https://github.com/revel/revel/issues/783)
+ - [x] Allow local reverse proxy access to jobs module status page for IPv4/6 #[481](https://github.com/revel/revel/issues/481) PR#[6](https://github.com/revel/modules/pull/6) PR#[7](https://github.com/revel/modules/pull/7)
+ - [x] Add default http.Status code for render methods. #[728](https://github.com/revel/revel/issues/728)
+ - [x] add domain for cookie #[770](https://github.com/revel/revel/issues/770) PR#[882](https://github.com/revel/revel/pull/882)
+ - [x] production mode panic bug #[831](https://github.com/revel/revel/issues/831) PR#[881](https://github.com/revel/revel/pull/881)
+ - [x] Fixes template loading order whether watcher is enabled or not #[844](https://github.com/revel/revel/issues/844)
+ - [x] Fixes reverse routing wildcard bug PR#[886](https://github.com/revel/revel/pull/886) #[869](https://github.com/revel/revel/issues/869)
+ - [x] Fixes router app start bug without routes. PR #[855](https://github.com/revel/revel/pull/855)
+ - [x] Friendly URL template errors; Fixes template `url` func "index out of range" when param is `undefined` #[811](https://github.com/revel/revel/issues/811) PR#[880](https://github.com/revel/revel/pull/880)
+ - [x] Make result compression conditional PR#[888](https://github.com/revel/revel/pull/888)
+ - [x] ensure routes are loaded before returning from OnAppStart callback PR#[884](https://github.com/revel/revel/pull/884)
+ - [x] Use "302 Found" HTTP code for redirect PR#[900](https://github.com/revel/revel/pull/900)
+ - [x] Fix broken fake app tests PR#[899](https://github.com/revel/revel/pull/899)
+ - [x] Optimize search of template names PR#[885](https://github.com/revel/revel/pull/885)
+- `revel/cmd`:
+ - [x] track current Revel version #[418](https://github.com/revel/revel/issues/418) PR#[858](https://github.com/revel/revel/pull/858)
+ - [x] log path error After revel build? #[763](https://github.com/revel/revel/issues/763)
+ - [x] Use a separate directory for revel project binaries #[17](https://github.com/revel/cmd/pull/17) #[819](https://github.com/revel/revel/issues/819)
+ - [x] Overwrite generated app files instead of deleting directory #[551](https://github.com/revel/revel/issues/551) PR#[23](https://github.com/revel/cmd/pull/23)
+- `revel/modules`:
+ - [x] Adds runtime pprof/trace support #[9](https://github.com/revel/modules/pull/9)
+- Community Goals:
+ - [x] Issue labels #[545](https://github.com/revel/revel/issues/545)
+ - [x] Sync up labels/milestones in other repos #[721](https://github.com/revel/revel/issues/721)
+ - [x] Update the Revel Manual to reflect current features
+ - [x] [revel/revel.github.io/32](https://github.com/revel/revel.github.io/issues/32)
+ - [x] [revel/revel.github.io/39](https://github.com/revel/revel.github.io/issues/39)
+ - [x] Docs are obsolete, inaccessible TestRequest.testSuite #[791](https://github.com/revel/revel/issues/791)
+ - [x] Some questions about revel & go docs #[793](https://github.com/revel/revel/issues/793)
+ - [x] RFCs to organize features #[827](https://github.com/revel/revel/issues/827)
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.11.3...v0.12.0)
+
+
+# v0.11.0
+@brendensoares released this on 2014-10-26
+
+Note, Revel 0.11 requires Go 1.3 or higher.
+
+Changes since v0.10:
+
+[BUG] #729 Adding define inside the template results in an error (Changes how template file name case insensitivity is handled)
+
+[ENH] #769 Add swap files to gitignore
+[ENH] #766 Added passing in build flags to the go build command
+[ENH] #761 Fixing cross-compiling issue #456 setting windows path from linux
+[ENH] #759 Include upload sample's tests in travis
+[ENH] #755 Changes c.Action to be the action method name's letter casing per #635
+[ENH] #754 Adds call stack display to runtime panic in browser to match console
+[ENH] #740 Redis Cache: Add timeouts.
+[ENH] #734 watcher: treat fsnotify Op as a bitmask
+[ENH] #731 Second struct in type revel fails to find the controller
+[ENH] #725 Testrunner: show response info
+[ENH] #723 Improved compilation errors and open file from error page
+[ENH] #720 Get testrunner path from config file
+[ENH] #707 Add log.colorize option to enable/disable colorize
+[ENH] #696 Revel file upload testing
+[ENH] #694 Install dependencies at build time
+[ENH] #693 Prefer extension over Accept header
+[ENH] #692 Update fsnotify to v1 API
+[ENH] #690 Support zero downtime restarts
+[ENH] #687 Tests: request override
+[ENH] #685 Persona sample tests and bugfix
+[ENH] #598 Added README file to Revel skeleton
+[ENH] #591 Realtime rebuild
+[ENH] #573 Add AppRoot to allow changing the root path of an application
+
+[FTR] #606 CSRF Support
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.10.0...v0.11.0)
+
+
+# v0.10.0
+@brendensoares released this on 2014-08-10
+
+Changes since v0.9.1:
+- [FTR] #641 - Add "X-HTTP-Method-Override" to router
+- [FTR] #583 - Added HttpMethodOverride filter to routes
+- [FTR] #540 - watcher flag for refresh on app start
+- [BUG] #681 - Case insensitive comparison for websocket upgrades (Fixes IE Websockets ...
+- [BUG] #668 - Compression: Properly close gzip/deflate
+- [BUG] #667 - Fix redis GetMulti and improve test coverage
+- [BUG] #664 - Is compression working correct?
+- [BUG] #657 - Redis Cache: panic when testing Ge
+- [BUG] #637 - RedisCache: fix Get/GetMulti error return
+- [BUG] #621 - Bugfix/router csv error
+- [BUG] #618 - Router throws exception when parsing line with multiple default string arguments
+- [BUG] #604 - Compression: Properly close gzip/deflate.
+- [BUG] #567 - Fixed regex pattern to properly require message files to have a dot in filename
+- [BUG] #566 - Compression fails ("unexpected EOF" in tests)
+- [BUG] #287 - Don't remove the parent folders containing generated code.
+- [BUG] #556 - fix for #534, also added url path to not found message
+- [BUG] #534 - Websocket route not found
+- [BUG] #343 - validation.Required(funtionCall).Key(...) - reflect.go:715: Failed to generate name for field.
+- [ENH] #643 - Documentation Fix in Skeleton for OnAppStart
+- [ENH] #674 - Removes custom `eq` template function
+- [ENH] #669 - Develop compress closenotifier
+- [ENH] #663 - fix for static content type not being set and defaulting to OS
+- [ENH] #658 - Minor: fix niggle with import statement
+- [ENH] #652 - Update the contributing guidelines
+- [ENH] #651 - Use upstream gomemcache again
+- [ENH] #650 - Go back to upstream memcached library
+- [ENH] #612 - Fix CI package error
+- [ENH] #611 - Fix "go vet" problems
+- [ENH] #610 - Added MakeMultipartRequest() to the TestSuite
+- [ENH] #608 - Develop compress closenotifier
+- [ENH] #596 - Expose redis cache options to config
+- [ENH] #581 - Make the option template tag type agnostic.
+- [ENH] #576 - Defer session instantiation to first set
+- [ENH] #565 - Fix #563 -- Some custom template funcs cannot be used in JavaScript cont...
+- [ENH] #563 - TemplateFuncs cannot be used in JavaScript context
+- [ENH] #561 - Fix missing extension from message file causing panic
+- [ENH] #560 - enhancement / templateFunc `firstof`
+- [ENH] #555 - adding symlink handling to the template loader and watcher processes
+- [ENH] #531 - Update app.conf.template
+- [ENH] #520 - Respect controller's Response.Status when action returns nil
+- [ENH] #519 - Link to issues
+- [ENH] #486 - Support for json compress
+- [ENH] #480 - Eq implementation in template.go still necessary ?
+- [ENH] #461 - Cron jobs not started until I pull a page
+- [ENH] #323 - disable session/set-cookie for `Static.Serve()`
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.9.1...v0.10.0)
+
+
+# v0.9.1
+@pushrax released this on 2014-03-02
+
+Minor patch release to address a couple bugs.
+
+Changes since v0.9.0:
+- [BUG] #529 - Wrong path was used to determine existence of `.git`
+- [BUG] #532 - Fix typo for new type `ValidEmail`
+
+The full list of commits can be found [here](https://github.com/revel/revel/compare/v0.9.0...v0.9.1).
+
+
+# v0.9.0
+@pushrax released this on 2014-02-26
+
+## Revel GitHub Organization
+
+We've moved development of the framework to the @revel GitHub organization, to help manage the project as Revel grows. The old import path is still valid, but will not be updated in the future.
+
+You'll need to manually update your apps to work with the new import path. This can be done by replacing all instances of `github.com/robfig/revel` with `github.com/revel/revel` in your app, and running:
+
+```
+$ cd your_app_folder
+$ go get -u github.com/howeyc/fsnotify # needs updating
+$ go get github.com/revel/revel
+$ go get github.com/revel/cmd/revel # command line tools have moved
+```
+
+**Note:** if you have references to `github.com/robfig/revel/revel` in any files, you need to replace them with `github.com/revel/cmd/revel` _before_ replacing `github.com/robfig/revel`! (note the prefix collision)
+
+If you have any trouble upgrading or notice something we missed, feel free to hop in the IRC channel (#revel on Freenode) or send the mailing list a message.
+
+Also note, the documentation is now at [revel.github.io](http://revel.github.io)!
+
+Changes since v0.8:
+- [BUG] #522 - `revel new` bug
+- [BUG] - Booking sample error
+- [BUG] #504 - File access via URL security issue
+- [BUG] #489 - Email validator bug
+- [BUG] #475 - File watcher infinite loop
+- [BUG] #333 - Extensions in routes break parameters
+- [FTR] #472 - Support for 3rd part app skeletons
+- [ENH] #512 - Per session expiration methods
+- [ENH] #496 - Type check renderArgs[CurrentLocalRenderArg]
+- [ENH] #490 - App.conf manual typo
+- [ENH] #487 - Make files executable on `revel build`
+- [ENH] #482 - Retain input values after form valdiation
+- [ENH] #473 - OnAppStart documentation
+- [ENH] #466 - JSON error template quoting fix
+- [ENH] #464 - Remove unneeded trace statement
+- [ENH] #457 - Remove unneeded trace
+- [ENH] #508 - Support arbitrary network types
+- [ENH] #516 - Add Date and Message-Id mail headers
+
+The full list of commits can be found [here](https://github.com/revel/revel/compare/v0.8...v0.9.0).
+
+
+# v0.8
+@pushrax released this on 2014-01-06
+
+Changes since v0.7:
+- [BUG] #379 - HTTP 500 error for not found public path files
+- [FTR] #424 - HTTP pprof support
+- [FTR] #346 - Redis Cache support
+- [FTR] #292 - SMTP Mailer
+- [ENH] #443 - Validator constructors to improve `v.Check()` usage
+- [ENH] #439 - Basic terminal output coloring
+- [ENH] #428 - Improve error message for missing `RenderArg`
+- [ENH] #422 - Route embedding for modules
+- [ENH] #413 - App version variable
+- [ENH] #153 - $GOPATH-wide file watching aka hot loading
+
+
+# v0.6
+@robfig released this on 2013-09-16
+
+
+
--- /dev/null
+## Contributing to Revel
+
+This describes how developers may contribute to Revel.
+
+## Mission
+
+Revel's mission is to provide a batteries-included framework for making large
+scale web application development as efficient and maintainable as possible.
+
+The design should be configurable and modular so that it can grow with the
+developer. However, it should provide a wonderful un-boxing experience and
+default configuration that can woo new developers and make simple web apps
+straight forward. The framework should have an opinion about how to do all of the
+common tasks in web development to reduce unnecessary cognitive load.
+
+Perhaps most important of all, Revel should be a joy to use. We want to reduce
+the time spent on tedious boilerplate functionality and increase the time
+available for creating polished solutions for your application's target users.
+
+## How to Contribute
+
+### Join the Community
+
+The first step to making Revel better is joining the community! You can find the
+community on:
+
+* [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) via [revel-framework@googlegroups.com](mailto:revel-framework@googlegroups.com)
+* [GitHub Issues](https://github.com/revel/revel/issues)
+* [StackOverflow Questions](http://stackoverflow.com/questions/tagged/revel)
+* [IRC](http://webchat.freenode.net/?channels=%23revel&uio=d4) via #revel on Freenode
+
+Once you've joined, there are many ways to contribute to Revel:
+
+* Report bugs (via GitHub)
+* Answer questions of other community members (via Google Groups or IRC)
+* Give feedback on new feature discussions (via GitHub and Google Groups)
+* Propose your own ideas (via Google Groups or GitHub)
+
+### How Revel is Developed
+
+We have begun to formalize the development process by adopting pragmatic
+practices such as:
+
+* Developing on the `develop` branch
+* Merging `develop` branch to `master` branch in 6 week iterations
+* Tagging releases with MAJOR.MINOR syntax (e.g. v0.8)
+** We may also tag MAJOR.MINOR.HOTFIX releases as needed (e.g. v0.8.1) to
+address urgent bugs. Such releases will not introduce or change functionality
+* Managing bugs, enhancements, features and release milestones via GitHub's Issue Tracker
+* Using feature branches to create pull requests
+* Discussing new features **before** hacking away at it
+
+
+### How to Correctly Fork
+
+Go uses the repository URL to import packages, so forking and `go get`ing the
+forked project **will not work**.
+
+Instead, follow these steps:
+
+1. Install Revel normally
+2. Fork Revel on GitHub
+3. Add your fork as a git remote
+
+Here's the commands to do so:
+```
+$ go get github.com/revel/revel # Install Revel
+$ cd $GOPATH/src/github.com/revel/revel # Change directory to revel repo
+$ git remote add fork git@github.com:$USER/revel.git # Add your fork as a remote, where $USER is your GitHub username
+```
+
+### Create a Feature Branch & Code Away!
+
+Now that you've properly installed and forked Revel, you are ready to start coding (assuming
+you have a validated your ideas with other community members)!
+
+In order to have your pull requests accepted, we recommend you make your changes to Revel on a
+new git branch. For example,
+```
+$ git checkout -b feature/useful-new-thing origin/develop # Create a new branch based on develop and switch to it
+$ ... # Make your changes and commit them
+$ git push fork feature/useful-new-thing # After new commits, push to your fork
+```
+
+### Format Your Code
+
+Remember to run `go fmt` before committing your changes.
+Many Go developers opt to have their editor run `go fmt` automatically when
+saving Go files.
+
+Additionally, follow the [core Go style conventions](https://code.google.com/p/go-wiki/wiki/CodeReviewComments)
+to have your pull requests accepted.
+
+### Write Tests (and Benchmarks for Bonus Points)
+
+Significant new features require tests. Besides unit tests, it is also possible
+to test a feature by exercising it in one of the sample apps and verifying its
+operation using that app's test suite. This has the added benefit of providing
+example code for developers to refer to.
+
+Benchmarks are helpful but not required.
+
+### Run the Tests
+
+Typically running the main set of unit tests will be sufficient:
+
+```
+$ go test github.com/revel/revel
+```
+
+Refer to the
+[Travis configuration](https://github.com/revel/revel/blob/master/.travis.yml)
+for the full set of tests. They take less than a minute to run.
+
+### Document Your Feature
+
+Due to the wide audience and shared nature of Revel, documentation is an essential
+addition to your new code. **Pull requests risk not being accepted** until proper
+documentation is created to detail how to make use of new functionality.
+
+The [Revel web site](http://revel.github.io/) is hosted on GitHub Pages and
+[built with Jekyll](https://help.github.com/articles/using-jekyll-with-pages).
+
+To develop the Jekyll site locally:
+
+ # Clone the documentation repository
+ $ git clone git@github.com:revel/revel.github.io
+ $ cd revel.github.io
+
+ # Install and run Jekyll to generate and serve the site
+ $ gem install jekyll kramdown
+ $ jekyll serve --watch
+
+ # Now load in your browser
+ $ open http://localhost:4000/
+
+Any changes you make to the site should be reflected within a few seconds.
+
+### Submit Pull Request
+
+Once you've done all of the above & pushed your changes to your fork, you can create a pull request for review and acceptance.
+
+## Potential Projects
+
+These are outstanding feature requests, roughly ordered by priority.
+Additionally, there are frequently smaller feature requests or items in the
+[issues](https://github.com/revel/revel/issues?labels=contributor+ready&page=1&state=open).
+
+1. Better ORM support. Provide more samples (or modules) and better documentation for setting up common situations like SQL database, Mongo, LevelDB, etc.
+1. Support for other templating languages (e.g. mustache, HAML). Make TemplateLoader pluggable. Use Pongo instead of vanilla Go templates (and update the samples)
+1. Test Fixtures
+1. Authenticity tokens for CSRF protection
+1. Coffeescript pre-processor. Could potentially use [otto](https://github.com/robertkrimen/otto) as a native Go method to compiling.
+1. SCSS/LESS pre-processor.
+1. GAE support. Some progress made in the 'appengine' branch -- the remaining piece is running the appengine services in development.
+1. More Form helpers (template funcs).
+1. A Mongo module (perhaps with a sample app)
+1. Deployment to OpenShift (support, documentation, etc)
+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/))
+1. ETags, cache controls
+1. A module or plugins for adding HTTP Basic Auth
+1. Allowing the app to hook into the source code processing step
--- /dev/null
+The MIT License (MIT)
+
+Copyright (C) 2012-2018 The Revel Framework Authors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+# Revel Framework
+
+[](http://travis-ci.org/revel/revel)
+[](LICENSE)
+[](https://goreportcard.com/report/github.com/revel/revel)
+
+A high productivity, full-stack web framework for the [Go language](http://www.golang.org).
+
+Current Version: 0.21.0 (2018-10-30)
+
+**Because of Default HTTP Server's graceful shutdown, Go 1.8+ is required.**
+
+## Quick Start
+
+Enter Go's path (format varies based on OS):
+
+ cd $GOPATH
+
+Install Revel:
+
+ go get -u github.com/revel/cmd/revel
+
+Create & Run your app:
+
+ revel new -a my-app -r
+
+Open http://localhost:9000 in your browser and you should see "It works!"
+
+
+## Community
+
+* [Gitter](https://gitter.im/revel/community)
+* [StackOverflow](http://stackoverflow.com/questions/tagged/revel)
+
+
+## Learn More
+
+* [Manual, Samples, Godocs, etc](http://revel.github.io)
+* [Apps using Revel](https://github.com/revel/revel/wiki/Apps-in-the-Wild)
+* [Articles Featuring Revel](https://github.com/revel/revel/wiki/Articles)
+
+## Contributing
+
+* [Contributing Code Guidelines](https://github.com/revel/revel/blob/master/CONTRIBUTING.md)
+* [Revel Contributors](https://github.com/revel/revel/graphs/contributors)
+
+## Contributors
+
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/0)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/1)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/2)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/3)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/4)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/5)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/6)
+[](https://sourcerer.io/fame/notzippy/revel/revel/links/7)
--- /dev/null
+package revel
+
+import (
+ "reflect"
+)
+
+// Autocalls any defined before and after methods on the target controller
+// If either calls returns a value then the result is returned
+func BeforeAfterFilter(c *Controller, fc []Filter) {
+ defer func() {
+ if resultValue := beforeAfterFilterInvoke(FINALLY, c); resultValue != nil && !resultValue.IsNil() {
+ c.Result = resultValue.Interface().(Result)
+ }
+ }()
+ defer func() {
+ if err := recover(); err != nil {
+ if resultValue := beforeAfterFilterInvoke(PANIC, c); resultValue != nil && !resultValue.IsNil() {
+ c.Result = resultValue.Interface().(Result)
+ }
+ panic(err)
+ }
+ }()
+ if resultValue := beforeAfterFilterInvoke(BEFORE, c); resultValue != nil && !resultValue.IsNil() {
+ c.Result = resultValue.Interface().(Result)
+ }
+ fc[0](c, fc[1:])
+ if resultValue := beforeAfterFilterInvoke(AFTER, c); resultValue != nil && !resultValue.IsNil() {
+ c.Result = resultValue.Interface().(Result)
+ }
+}
+
+func beforeAfterFilterInvoke(method When, c *Controller) (r *reflect.Value) {
+
+ if c.Type == nil {
+ return
+ }
+ var index []*ControllerFieldPath
+ switch method {
+ case BEFORE:
+ index = c.Type.ControllerEvents.Before
+ case AFTER:
+ index = c.Type.ControllerEvents.After
+ case FINALLY:
+ index = c.Type.ControllerEvents.Finally
+ case PANIC:
+ index = c.Type.ControllerEvents.Panic
+ }
+
+ if len(index) == 0 {
+ return
+ }
+ for _, function := range index {
+ result := function.Invoke(reflect.ValueOf(c.AppController), nil)[0]
+ if !result.IsNil() {
+ return &result
+ }
+ }
+
+ return
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// A Binder translates between string parameters and Go data structures.
+type Binder struct {
+ // Bind takes the name and type of the desired parameter and constructs it
+ // from one or more values from Params.
+ //
+ // Example
+ //
+ // Request:
+ // url?id=123&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=rob
+ //
+ // Action:
+ // Example.Action(id int, ol []int, ul []string, user User)
+ //
+ // Calls:
+ // Bind(params, "id", int): 123
+ // Bind(params, "ol", []int): {1, 2}
+ // Bind(params, "ul", []string): {"str", "array"}
+ // Bind(params, "user", User): User{Name:"rob"}
+ //
+ // Note that only exported struct fields may be bound.
+ Bind func(params *Params, name string, typ reflect.Type) reflect.Value
+
+ // Unbind serializes a given value to one or more URL parameters of the given
+ // name.
+ Unbind func(output map[string]string, name string, val interface{})
+}
+
+var binderLog = RevelLog.New("section", "binder")
+
+// ValueBinder is adapter for easily making one-key-value binders.
+func ValueBinder(f func(value string, typ reflect.Type) reflect.Value) func(*Params, string, reflect.Type) reflect.Value {
+ return func(params *Params, name string, typ reflect.Type) reflect.Value {
+ vals, ok := params.Values[name]
+ if !ok || len(vals) == 0 {
+ return reflect.Zero(typ)
+ }
+ return f(vals[0], typ)
+ }
+}
+
+// Revel's default date and time constants
+const (
+ DefaultDateFormat = "2006-01-02"
+ DefaultDateTimeFormat = "2006-01-02 15:04"
+)
+
+// Binders type and kind definition
+var (
+ // These are the lookups to find a Binder for any type of data.
+ // The most specific binder found will be used (Type before Kind)
+ TypeBinders = make(map[reflect.Type]Binder)
+ KindBinders = make(map[reflect.Kind]Binder)
+
+ // Applications can add custom time formats to this array, and they will be
+ // automatically attempted when binding a time.Time.
+ TimeFormats = []string{}
+
+ DateFormat string
+ DateTimeFormat string
+ TimeZone = time.UTC
+
+ IntBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ if len(val) == 0 {
+ return reflect.Zero(typ)
+ }
+ intValue, err := strconv.ParseInt(val, 10, 64)
+ if err != nil {
+ binderLog.Warn("IntBinder Conversion Error", "error", err)
+ return reflect.Zero(typ)
+ }
+ pValue := reflect.New(typ)
+ pValue.Elem().SetInt(intValue)
+ return pValue.Elem()
+ }),
+ Unbind: func(output map[string]string, key string, val interface{}) {
+ output[key] = fmt.Sprintf("%d", val)
+ },
+ }
+
+ UintBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ if len(val) == 0 {
+ return reflect.Zero(typ)
+ }
+ uintValue, err := strconv.ParseUint(val, 10, 64)
+ if err != nil {
+ binderLog.Warn("UintBinder Conversion Error", "error", err)
+ return reflect.Zero(typ)
+ }
+ pValue := reflect.New(typ)
+ pValue.Elem().SetUint(uintValue)
+ return pValue.Elem()
+ }),
+ Unbind: func(output map[string]string, key string, val interface{}) {
+ output[key] = fmt.Sprintf("%d", val)
+ },
+ }
+
+ FloatBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ if len(val) == 0 {
+ return reflect.Zero(typ)
+ }
+ floatValue, err := strconv.ParseFloat(val, 64)
+ if err != nil {
+ binderLog.Warn("FloatBinder Conversion Error", "error", err)
+ return reflect.Zero(typ)
+ }
+ pValue := reflect.New(typ)
+ pValue.Elem().SetFloat(floatValue)
+ return pValue.Elem()
+ }),
+ Unbind: func(output map[string]string, key string, val interface{}) {
+ output[key] = fmt.Sprintf("%f", val)
+ },
+ }
+
+ StringBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ return reflect.ValueOf(val)
+ }),
+ Unbind: func(output map[string]string, name string, val interface{}) {
+ output[name] = val.(string)
+ },
+ }
+
+ // Booleans support a various value formats,
+ // refer `revel.Atob` method.
+ BoolBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ return reflect.ValueOf(Atob(val))
+ }),
+ Unbind: func(output map[string]string, name string, val interface{}) {
+ output[name] = fmt.Sprintf("%t", val)
+ },
+ }
+
+ PointerBinder = Binder{
+ Bind: func(params *Params, name string, typ reflect.Type) reflect.Value {
+ v := Bind(params, name, typ.Elem())
+ if v.CanAddr() {
+ return v.Addr()
+ }
+
+ return v
+ },
+ Unbind: func(output map[string]string, name string, val interface{}) {
+ Unbind(output, name, reflect.ValueOf(val).Elem().Interface())
+ },
+ }
+
+ TimeBinder = Binder{
+ Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+ for _, f := range TimeFormats {
+ if r, err := time.ParseInLocation(f, val, TimeZone); err == nil {
+ return reflect.ValueOf(r)
+ }
+ }
+ return reflect.Zero(typ)
+ }),
+ Unbind: func(output map[string]string, name string, val interface{}) {
+ var (
+ t = val.(time.Time)
+ format = DateTimeFormat
+ h, m, s = t.Clock()
+ )
+ if h == 0 && m == 0 && s == 0 {
+ format = DateFormat
+ }
+ output[name] = t.Format(format)
+ },
+ }
+
+ MapBinder = Binder{
+ Bind: bindMap,
+ Unbind: unbindMap,
+ }
+)
+
+// Used to keep track of the index for individual keyvalues.
+type sliceValue struct {
+ index int // Index extracted from brackets. If -1, no index was provided.
+ value reflect.Value // the bound value for this slice element.
+}
+
+// This function creates a slice of the given type, Binds each of the individual
+// elements, and then sets them to their appropriate location in the slice.
+// If elements are provided without an explicit index, they are added (in
+// unspecified order) to the end of the slice.
+func bindSlice(params *Params, name string, typ reflect.Type) reflect.Value {
+ // Collect an array of slice elements with their indexes (and the max index).
+ maxIndex := -1
+ numNoIndex := 0
+ sliceValues := []sliceValue{}
+
+ // Factor out the common slice logic (between form values and files).
+ processElement := func(key string, vals []string, files []*multipart.FileHeader) {
+ if !strings.HasPrefix(key, name+"[") {
+ return
+ }
+
+ // Extract the index, and the index where a sub-key starts. (e.g. field[0].subkey)
+ index := -1
+ leftBracket, rightBracket := len(name), strings.Index(key[len(name):], "]")+len(name)
+ if rightBracket > leftBracket+1 {
+ index, _ = strconv.Atoi(key[leftBracket+1 : rightBracket])
+ }
+ subKeyIndex := rightBracket + 1
+
+ // Handle the indexed case.
+ if index > -1 {
+ if index > maxIndex {
+ maxIndex = index
+ }
+ sliceValues = append(sliceValues, sliceValue{
+ index: index,
+ value: Bind(params, key[:subKeyIndex], typ.Elem()),
+ })
+ return
+ }
+
+ // It's an un-indexed element. (e.g. element[])
+ numNoIndex += len(vals) + len(files)
+ for _, val := range vals {
+ // Unindexed values can only be direct-bound.
+ sliceValues = append(sliceValues, sliceValue{
+ index: -1,
+ value: BindValue(val, typ.Elem()),
+ })
+ }
+
+ for _, fileHeader := range files {
+ sliceValues = append(sliceValues, sliceValue{
+ index: -1,
+ value: BindFile(fileHeader, typ.Elem()),
+ })
+ }
+ }
+
+ for key, vals := range params.Values {
+ processElement(key, vals, nil)
+ }
+ for key, fileHeaders := range params.Files {
+ processElement(key, nil, fileHeaders)
+ }
+
+ resultArray := reflect.MakeSlice(typ, maxIndex+1, maxIndex+1+numNoIndex)
+ for _, sv := range sliceValues {
+ if sv.index != -1 {
+ resultArray.Index(sv.index).Set(sv.value)
+ } else {
+ resultArray = reflect.Append(resultArray, sv.value)
+ }
+ }
+
+ return resultArray
+}
+
+// Break on dots and brackets.
+// e.g. bar => "bar", bar.baz => "bar", bar[0] => "bar"
+func nextKey(key string) string {
+ fieldLen := strings.IndexAny(key, ".[")
+ if fieldLen == -1 {
+ return key
+ }
+ return key[:fieldLen]
+}
+
+func unbindSlice(output map[string]string, name string, val interface{}) {
+ v := reflect.ValueOf(val)
+ for i := 0; i < v.Len(); i++ {
+ Unbind(output, fmt.Sprintf("%s[%d]", name, i), v.Index(i).Interface())
+ }
+}
+
+func bindStruct(params *Params, name string, typ reflect.Type) reflect.Value {
+ resultPointer := reflect.New(typ)
+ result := resultPointer.Elem()
+ if params.JSON != nil {
+ // Try to inject the response as a json into the created result
+ if err := json.Unmarshal(params.JSON, resultPointer.Interface()); err != nil {
+ binderLog.Error("bindStruct Unable to unmarshal request", "name", name, "error", err, "data", string(params.JSON))
+ }
+ return result
+ }
+ fieldValues := make(map[string]reflect.Value)
+ for key := range params.Values {
+ if !strings.HasPrefix(key, name+".") {
+ continue
+ }
+
+ // Get the name of the struct property.
+ // Strip off the prefix. e.g. foo.bar.baz => bar.baz
+ suffix := key[len(name)+1:]
+ fieldName := nextKey(suffix)
+ fieldLen := len(fieldName)
+
+ if _, ok := fieldValues[fieldName]; !ok {
+ // Time to bind this field. Get it and make sure we can set it.
+ fieldValue := result.FieldByName(fieldName)
+ if !fieldValue.IsValid() {
+ binderLog.Warn("bindStruct Field not found", "name", fieldName)
+ continue
+ }
+ if !fieldValue.CanSet() {
+ binderLog.Warn("bindStruct Field not settable", "name", fieldName)
+ continue
+ }
+ boundVal := Bind(params, key[:len(name)+1+fieldLen], fieldValue.Type())
+ fieldValue.Set(boundVal)
+ fieldValues[fieldName] = boundVal
+ }
+ }
+
+ return result
+}
+
+func unbindStruct(output map[string]string, name string, iface interface{}) {
+ val := reflect.ValueOf(iface)
+ typ := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ structField := typ.Field(i)
+ fieldValue := val.Field(i)
+
+ // PkgPath is specified to be empty exactly for exported fields.
+ if structField.PkgPath == "" {
+ Unbind(output, fmt.Sprintf("%s.%s", name, structField.Name), fieldValue.Interface())
+ }
+ }
+}
+
+// Helper that returns an upload of the given name, or nil.
+func getMultipartFile(params *Params, name string) multipart.File {
+ for _, fileHeader := range params.Files[name] {
+ file, err := fileHeader.Open()
+ if err == nil {
+ return file
+ }
+ binderLog.Warn("getMultipartFile: Failed to open uploaded file", "name", name, "error", err)
+ }
+ return nil
+}
+
+func bindFile(params *Params, name string, typ reflect.Type) reflect.Value {
+ reader := getMultipartFile(params, name)
+ if reader == nil {
+ return reflect.Zero(typ)
+ }
+
+ // If it's already stored in a temp file, just return that.
+ if osFile, ok := reader.(*os.File); ok {
+ return reflect.ValueOf(osFile)
+ }
+
+ // Otherwise, have to store it.
+ tmpFile, err := ioutil.TempFile("", "revel-upload")
+ if err != nil {
+ binderLog.Warn("bindFile: Failed to create a temp file to store upload", "name", name, "error", err)
+ return reflect.Zero(typ)
+ }
+
+ // Register it to be deleted after the request is done.
+ params.tmpFiles = append(params.tmpFiles, tmpFile)
+
+ _, err = io.Copy(tmpFile, reader)
+ if err != nil {
+ binderLog.Warn("bindFile: Failed to copy upload to temp file", "name", name, "error", err)
+ return reflect.Zero(typ)
+ }
+
+ _, err = tmpFile.Seek(0, 0)
+ if err != nil {
+ binderLog.Warn("bindFile: Failed to seek to beginning of temp file", "name", name, "error", err)
+ return reflect.Zero(typ)
+ }
+
+ return reflect.ValueOf(tmpFile)
+}
+
+func bindByteArray(params *Params, name string, typ reflect.Type) reflect.Value {
+ if reader := getMultipartFile(params, name); reader != nil {
+ b, err := ioutil.ReadAll(reader)
+ if err == nil {
+ return reflect.ValueOf(b)
+ }
+ binderLog.Warn("bindByteArray: Error reading uploaded file contents", "name", name, "error", err)
+ }
+ return reflect.Zero(typ)
+}
+
+func bindReadSeeker(params *Params, name string, typ reflect.Type) reflect.Value {
+ if reader := getMultipartFile(params, name); reader != nil {
+ return reflect.ValueOf(reader.(io.ReadSeeker))
+ }
+ return reflect.Zero(typ)
+}
+
+// bindMap converts parameters using map syntax into the corresponding map. e.g.:
+// params["a[5]"]=foo, name="a", typ=map[int]string => map[int]string{5: "foo"}
+func bindMap(params *Params, name string, typ reflect.Type) reflect.Value {
+ var (
+ keyType = typ.Key()
+ valueType = typ.Elem()
+ resultPtr = reflect.New(reflect.MapOf(keyType, valueType))
+ result = resultPtr.Elem()
+ )
+ result.Set(reflect.MakeMap(typ))
+ if params.JSON != nil {
+ // Try to inject the response as a json into the created result
+ if err := json.Unmarshal(params.JSON, resultPtr.Interface()); err != nil {
+ binderLog.Warn("bindMap: Unable to unmarshal request", "name", name, "error", err)
+ }
+ return result
+ }
+
+ for paramName := range params.Values {
+ // The paramName string must start with the value in the "name" parameter,
+ // otherwise there is no way the parameter is part of the map
+ if !strings.HasPrefix(paramName, name) {
+ continue
+ }
+
+ suffix := paramName[len(name)+1:]
+ fieldName := nextKey(suffix)
+ if fieldName != "" {
+ fieldName = fieldName[:len(fieldName)-1]
+ }
+ if !strings.HasPrefix(paramName, name+"["+fieldName+"]") {
+ continue
+ }
+
+ result.SetMapIndex(BindValue(fieldName, keyType), Bind(params, name+"["+fieldName+"]", valueType))
+ }
+ return result
+}
+
+func unbindMap(output map[string]string, name string, iface interface{}) {
+ mapValue := reflect.ValueOf(iface)
+ for _, key := range mapValue.MapKeys() {
+ Unbind(output, name+"["+fmt.Sprintf("%v", key.Interface())+"]",
+ mapValue.MapIndex(key).Interface())
+ }
+}
+
+// Bind takes the name and type of the desired parameter and constructs it
+// from one or more values from Params.
+// Returns the zero value of the type upon any sort of failure.
+func Bind(params *Params, name string, typ reflect.Type) reflect.Value {
+ if binder, found := binderForType(typ); found {
+ return binder.Bind(params, name, typ)
+ }
+ return reflect.Zero(typ)
+}
+
+func BindValue(val string, typ reflect.Type) reflect.Value {
+ return Bind(&Params{Values: map[string][]string{"": {val}}}, "", typ)
+}
+
+func BindFile(fileHeader *multipart.FileHeader, typ reflect.Type) reflect.Value {
+ return Bind(&Params{Files: map[string][]*multipart.FileHeader{"": {fileHeader}}}, "", typ)
+}
+
+func Unbind(output map[string]string, name string, val interface{}) {
+ if binder, found := binderForType(reflect.TypeOf(val)); found {
+ if binder.Unbind != nil {
+ binder.Unbind(output, name, val)
+ } else {
+ binderLog.Error("Unbind: Unable to unmarshal request", "name", name, "value", val)
+ }
+ }
+}
+
+func binderForType(typ reflect.Type) (Binder, bool) {
+ binder, ok := TypeBinders[typ]
+ if !ok {
+ binder, ok = KindBinders[typ.Kind()]
+ if !ok {
+ binderLog.Error("binderForType: no binder for type", "type", typ)
+ return Binder{}, false
+ }
+ }
+ return binder, true
+}
+
+// Sadly, the binder lookups can not be declared initialized -- that results in
+// an "initialization loop" compile error.
+func init() {
+ KindBinders[reflect.Int] = IntBinder
+ KindBinders[reflect.Int8] = IntBinder
+ KindBinders[reflect.Int16] = IntBinder
+ KindBinders[reflect.Int32] = IntBinder
+ KindBinders[reflect.Int64] = IntBinder
+
+ KindBinders[reflect.Uint] = UintBinder
+ KindBinders[reflect.Uint8] = UintBinder
+ KindBinders[reflect.Uint16] = UintBinder
+ KindBinders[reflect.Uint32] = UintBinder
+ KindBinders[reflect.Uint64] = UintBinder
+
+ KindBinders[reflect.Float32] = FloatBinder
+ KindBinders[reflect.Float64] = FloatBinder
+
+ KindBinders[reflect.String] = StringBinder
+ KindBinders[reflect.Bool] = BoolBinder
+ KindBinders[reflect.Slice] = Binder{bindSlice, unbindSlice}
+ KindBinders[reflect.Struct] = Binder{bindStruct, unbindStruct}
+ KindBinders[reflect.Ptr] = PointerBinder
+ KindBinders[reflect.Map] = MapBinder
+
+ TypeBinders[reflect.TypeOf(time.Time{})] = TimeBinder
+
+ // Uploads
+ TypeBinders[reflect.TypeOf(&os.File{})] = Binder{bindFile, nil}
+ TypeBinders[reflect.TypeOf([]byte{})] = Binder{bindByteArray, nil}
+ TypeBinders[reflect.TypeOf((*io.Reader)(nil)).Elem()] = Binder{bindReadSeeker, nil}
+ TypeBinders[reflect.TypeOf((*io.ReadSeeker)(nil)).Elem()] = Binder{bindReadSeeker, nil}
+
+ OnAppStart(func() {
+ DateTimeFormat = Config.StringDefault("format.datetime", DefaultDateTimeFormat)
+ DateFormat = Config.StringDefault("format.date", DefaultDateFormat)
+ TimeFormats = append(TimeFormats, DateTimeFormat, DateFormat)
+ })
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+)
+
+type A struct {
+ ID int
+ Name string
+ B B
+ private int
+}
+
+type B struct {
+ Extra string
+}
+
+var (
+ ParamTestValues = map[string][]string{
+ "int": {"1"},
+ "int8": {"1"},
+ "int16": {"1"},
+ "int32": {"1"},
+ "int64": {"1"},
+ "uint": {"1"},
+ "uint8": {"1"},
+ "uint16": {"1"},
+ "uint32": {"1"},
+ "uint64": {"1"},
+ "float32": {"1.000000"},
+ "float64": {"1.000000"},
+ "str": {"hello"},
+ "bool-true": {"true"},
+ "bool-1": {"1"},
+ "bool-on": {"on"},
+ "bool-false": {"false"},
+ "bool-0": {"0"},
+ "bool-0.0": {"0.0"},
+ "bool-off": {"off"},
+ "bool-f": {"f"},
+ "date": {"1982-07-09"},
+ "datetime": {"1982-07-09 21:30"},
+ "customDate": {"07/09/1982"},
+ "arr[0]": {"1"},
+ "arr[1]": {"2"},
+ "arr[3]": {"3"},
+ "uarr[]": {"1", "2"},
+ "arruarr[0][]": {"1", "2"},
+ "arruarr[1][]": {"3", "4"},
+ "2darr[0][0]": {"0"},
+ "2darr[0][1]": {"1"},
+ "2darr[1][0]": {"10"},
+ "2darr[1][1]": {"11"},
+ "A.ID": {"123"},
+ "A.Name": {"rob"},
+ "B.ID": {"123"},
+ "B.Name": {"rob"},
+ "B.B.Extra": {"hello"},
+ "pB.ID": {"123"},
+ "pB.Name": {"rob"},
+ "pB.B.Extra": {"hello"},
+ "priv.private": {"123"},
+ "arrC[0].ID": {"5"},
+ "arrC[0].Name": {"rob"},
+ "arrC[0].B.Extra": {"foo"},
+ "arrC[1].ID": {"8"},
+ "arrC[1].Name": {"bill"},
+ "m[a]": {"foo"},
+ "m[b]": {"bar"},
+ "m2[1]": {"foo"},
+ "m2[2]": {"bar"},
+ "m3[a]": {"1"},
+ "m3[b]": {"2"},
+ "m4[a].ID": {"1"},
+ "m4[a].Name": {"foo"},
+ "m4[b].ID": {"2"},
+ "m4[b].Name": {"bar"},
+ "mapWithAMuchLongerName[a].ID": {"1"},
+ "mapWithAMuchLongerName[a].Name": {"foo"},
+ "mapWithAMuchLongerName[b].ID": {"2"},
+ "mapWithAMuchLongerName[b].Name": {"bar"},
+ "invalidInt": {"xyz"},
+ "invalidInt2": {""},
+ "invalidBool": {"xyz"},
+ "invalidArr": {"xyz"},
+ "int8-overflow": {"1024"},
+ "uint8-overflow": {"1024"},
+ }
+
+ testDate = time.Date(1982, time.July, 9, 0, 0, 0, 0, time.UTC)
+ testDatetime = time.Date(1982, time.July, 9, 21, 30, 0, 0, time.UTC)
+)
+
+var binderTestCases = map[string]interface{}{
+ "int": 1,
+ "int8": int8(1),
+ "int16": int16(1),
+ "int32": int32(1),
+ "int64": int64(1),
+ "uint": 1,
+ "uint8": uint8(1),
+ "uint16": uint16(1),
+ "uint32": uint32(1),
+ "uint64": uint64(1),
+ "float32": float32(1.0),
+ "float64": float64(1.0),
+ "str": "hello",
+ "bool-true": true,
+ "bool-1": true,
+ "bool-on": true,
+ "bool-false": false,
+ "bool-0": false,
+ "bool-0.0": false,
+ "bool-off": false,
+ "bool-f": false,
+ "date": testDate,
+ "datetime": testDatetime,
+ "customDate": testDate,
+ "arr": []int{1, 2, 0, 3},
+ "uarr": []int{1, 2},
+ "arruarr": [][]int{{1, 2}, {3, 4}},
+ "2darr": [][]int{{0, 1}, {10, 11}},
+ "A": A{ID: 123, Name: "rob"},
+ "B": A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+ "pB": &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+ "arrC": []A{
+ {
+ ID: 5,
+ Name: "rob",
+ B: B{"foo"},
+ },
+ {
+ ID: 8,
+ Name: "bill",
+ },
+ },
+ "m": map[string]string{"a": "foo", "b": "bar"},
+ "m2": map[int]string{1: "foo", 2: "bar"},
+ "m3": map[string]int{"a": 1, "b": 2},
+ "m4": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+ // NOTE: We also include a map with a longer name than the others since this has caused problems
+ // described in github issue #1285, resolved in pull request #1344. This test case should
+ // prevent regression.
+ "mapWithAMuchLongerName": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+ // TODO: Tests that use TypeBinders
+
+ // Invalid value tests (the result should always be the zero value for that type)
+ // The point of these is to ensure that invalid user input does not cause panics.
+ "invalidInt": 0,
+ "invalidInt2": 0,
+ "invalidBool": true,
+ "invalidArr": []int{},
+ "priv": A{},
+ "int8-overflow": int8(0),
+ "uint8-overflow": uint8(0),
+}
+
+// Types that files may be bound to, and a func that can read the content from
+// that type.
+// TODO: Is there any way to create a slice, given only the element Type?
+var fileBindings = []struct{ val, arrval, f interface{} }{
+ {(**os.File)(nil), []*os.File{}, ioutil.ReadAll},
+ {(*[]byte)(nil), [][]byte{}, func(b []byte) []byte { return b }},
+ {(*io.Reader)(nil), []io.Reader{}, ioutil.ReadAll},
+ {(*io.ReadSeeker)(nil), []io.ReadSeeker{}, ioutil.ReadAll},
+}
+
+func TestJsonBinder(t *testing.T) {
+ // create a structure to be populated
+ {
+ d, _ := json.Marshal(map[string]int{"a": 1})
+ params := &Params{JSON: d}
+ foo := struct{ A int }{}
+ c := NewTestController(nil, getMultipartRequest())
+
+ ParseParams(params, NewRequest(c.Request.In))
+ actual := Bind(params, "test", reflect.TypeOf(foo))
+ valEq(t, "TestJsonBinder", reflect.ValueOf(actual.Interface().(struct{ A int }).A), reflect.ValueOf(1))
+ }
+ {
+ d, _ := json.Marshal(map[string]interface{}{"a": map[string]int{"b": 45}})
+ params := &Params{JSON: d}
+ testMap := map[string]interface{}{}
+ actual := Bind(params, "test", reflect.TypeOf(testMap)).Interface().(map[string]interface{})
+ if actual["a"].(map[string]interface{})["b"].(float64) != 45 {
+ t.Errorf("Failed to fetch map value %#v", actual["a"])
+ }
+ // Check to see if a named map works
+ actualb := Bind(params, "test", reflect.TypeOf(map[string]map[string]float64{})).Interface().(map[string]map[string]float64)
+ if actualb["a"]["b"] != 45 {
+ t.Errorf("Failed to fetch map value %#v", actual["a"])
+ }
+
+ }
+}
+
+func TestBinder(t *testing.T) {
+ // Reuse the mvc_test.go multipart request to test the binder.
+ params := &Params{}
+ c := NewTestController(nil, getMultipartRequest())
+ ParseParams(params, NewRequest(c.Request.In))
+ params.Values = ParamTestValues
+
+ // Values
+ for k, v := range binderTestCases {
+ actual := Bind(params, k, reflect.TypeOf(v))
+ expected := reflect.ValueOf(v)
+ valEq(t, k, actual, expected)
+ }
+
+ // Files
+
+ // Get the keys in sorted order to make the expectation right.
+ keys := []string{}
+ for k := range expectedFiles {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ expectedBoundFiles := make(map[string][]fh)
+ for _, k := range keys {
+ fhs := expectedFiles[k]
+ k := nextKey(k)
+ expectedBoundFiles[k] = append(expectedBoundFiles[k], fhs...)
+ }
+
+ for k, fhs := range expectedBoundFiles {
+
+ if len(fhs) == 1 {
+ // Test binding single files to: *os.File, []byte, io.Reader, io.ReadSeeker
+ for _, binding := range fileBindings {
+ typ := reflect.TypeOf(binding.val).Elem()
+ actual := Bind(params, k, typ)
+ if !actual.IsValid() || (actual.Kind() == reflect.Interface && actual.IsNil()) {
+ t.Errorf("%s (%s) - Returned nil.", k, typ)
+ continue
+ }
+ returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual})
+ valEq(t, k, returns[0], reflect.ValueOf(fhs[0].content))
+ }
+
+ } else {
+ // Test binding multi to:
+ // []*os.File, [][]byte, []io.Reader, []io.ReadSeeker
+ for _, binding := range fileBindings {
+ typ := reflect.TypeOf(binding.arrval)
+ actual := Bind(params, k, typ)
+ if actual.Len() != len(fhs) {
+ t.Fatalf("%s (%s) - Number of files: (expected) %d != %d (actual)",
+ k, typ, len(fhs), actual.Len())
+ }
+ for i := range fhs {
+ returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual.Index(i)})
+ if !returns[0].IsValid() {
+ t.Errorf("%s (%s) - Returned nil.", k, typ)
+ continue
+ }
+ valEq(t, k, returns[0], reflect.ValueOf(fhs[i].content))
+ }
+ }
+ }
+ }
+}
+
+// Unbinding tests
+
+var unbinderTestCases = map[string]interface{}{
+ "int": 1,
+ "int8": int8(1),
+ "int16": int16(1),
+ "int32": int32(1),
+ "int64": int64(1),
+ "uint": 1,
+ "uint8": uint8(1),
+ "uint16": uint16(1),
+ "uint32": uint32(1),
+ "uint64": uint64(1),
+ "float32": float32(1.0),
+ "float64": float64(1.0),
+ "str": "hello",
+ "bool-true": true,
+ "bool-false": false,
+ "date": testDate,
+ "datetime": testDatetime,
+ "arr": []int{1, 2, 0, 3},
+ "2darr": [][]int{{0, 1}, {10, 11}},
+ "A": A{ID: 123, Name: "rob"},
+ "B": A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+ "pB": &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+ "arrC": []A{
+ {
+ ID: 5,
+ Name: "rob",
+ B: B{"foo"},
+ },
+ {
+ ID: 8,
+ Name: "bill",
+ },
+ },
+ "m": map[string]string{"a": "foo", "b": "bar"},
+ "m2": map[int]string{1: "foo", 2: "bar"},
+ "m3": map[string]int{"a": 1, "b": 2},
+}
+
+// Some of the unbinding results are not exactly what is in ParamTestValues, since it
+// serializes implicit zero values explicitly.
+var unbinderOverrideAnswers = map[string]map[string]string{
+ "arr": {
+ "arr[0]": "1",
+ "arr[1]": "2",
+ "arr[2]": "0",
+ "arr[3]": "3",
+ },
+ "A": {
+ "A.ID": "123",
+ "A.Name": "rob",
+ "A.B.Extra": "",
+ },
+ "arrC": {
+ "arrC[0].ID": "5",
+ "arrC[0].Name": "rob",
+ "arrC[0].B.Extra": "foo",
+ "arrC[1].ID": "8",
+ "arrC[1].Name": "bill",
+ "arrC[1].B.Extra": "",
+ },
+ "m": {"m[a]": "foo", "m[b]": "bar"},
+ "m2": {"m2[1]": "foo", "m2[2]": "bar"},
+ "m3": {"m3[a]": "1", "m3[b]": "2"},
+}
+
+func TestUnbinder(t *testing.T) {
+ for k, v := range unbinderTestCases {
+ actual := make(map[string]string)
+ Unbind(actual, k, v)
+
+ // Get the expected key/values.
+ expected, ok := unbinderOverrideAnswers[k]
+ if !ok {
+ expected = make(map[string]string)
+ for k2, v2 := range ParamTestValues {
+ if k == k2 || strings.HasPrefix(k2, k+".") || strings.HasPrefix(k2, k+"[") {
+ expected[k2] = v2[0]
+ }
+ }
+ }
+
+ // Compare length and values.
+ if len(actual) != len(expected) {
+ t.Errorf("Length mismatch\nExpected length %d, actual %d\nExpected: %s\nActual: %s",
+ len(expected), len(actual), expected, actual)
+ }
+ for k, v := range actual {
+ if expected[k] != v {
+ t.Errorf("Value mismatch.\nExpected: %s\nActual: %s", expected, actual)
+ }
+ }
+ }
+}
+
+// Helpers
+
+func valEq(t *testing.T, name string, actual, expected reflect.Value) {
+ switch expected.Kind() {
+ case reflect.Slice:
+ // Check the type/length/element type
+ if !eq(t, name+" (type)", actual.Kind(), expected.Kind()) ||
+ !eq(t, name+" (len)", actual.Len(), expected.Len()) ||
+ !eq(t, name+" (elem)", actual.Type().Elem(), expected.Type().Elem()) {
+ return
+ }
+
+ // Check value equality for each element.
+ for i := 0; i < actual.Len(); i++ {
+ valEq(t, fmt.Sprintf("%s[%d]", name, i), actual.Index(i), expected.Index(i))
+ }
+
+ case reflect.Ptr:
+ // Check equality on the element type.
+ valEq(t, name, actual.Elem(), expected.Elem())
+ case reflect.Map:
+ if !eq(t, name+" (len)", actual.Len(), expected.Len()) {
+ return
+ }
+ for _, key := range expected.MapKeys() {
+ expectedValue := expected.MapIndex(key)
+ actualValue := actual.MapIndex(key)
+ if actualValue.IsValid() {
+ valEq(t, fmt.Sprintf("%s[%s]", name, key), actualValue, expectedValue)
+ } else {
+ t.Errorf("Expected key %s not found", key)
+ }
+ }
+ default:
+ eq(t, name, actual.Interface(), expected.Interface())
+ }
+}
+
+func init() {
+ DateFormat = DefaultDateFormat
+ DateTimeFormat = DefaultDateTimeFormat
+ TimeFormats = append(TimeFormats, DefaultDateFormat, DefaultDateTimeFormat, "01/02/2006")
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "errors"
+ "time"
+)
+
+// Length of time to cache an item.
+const (
+ DefaultExpiryTime = time.Duration(0)
+ ForEverNeverExpiry = time.Duration(-1)
+)
+
+// Getter is an interface for getting / decoding an element from a cache.
+type Getter interface {
+ // Get the content associated with the given key. decoding it into the given
+ // pointer.
+ //
+ // Returns:
+ // - nil if the value was successfully retrieved and ptrValue set
+ // - ErrCacheMiss if the value was not in the cache
+ // - an implementation specific error otherwise
+ Get(key string, ptrValue interface{}) error
+}
+
+// Cache is an interface to an expiring cache. It behaves (and is modeled) like
+// the Memcached interface. It is keyed by strings (250 bytes at most).
+//
+// Many callers will make exclusive use of Set and Get, but more exotic
+// functions are also available.
+//
+// Example
+//
+// Here is a typical Get/Set interaction:
+//
+// var items []*Item
+// if err := cache.Get("items", &items); err != nil {
+// items = loadItems()
+// go cache.Set("items", items, cache.DefaultExpiryTime)
+// }
+//
+// Note that the caller will frequently not wait for Set() to complete.
+//
+// Errors
+//
+// It is assumed that callers will infrequently check returned errors, since any
+// request should be fulfillable without finding anything in the cache. As a
+// result, all errors other than ErrCacheMiss and ErrNotStored will be logged to
+// revel.ERROR, so that the developer does not need to check the return value to
+// discover things like deserialization or connection errors.
+type Cache interface {
+ // The Cache implements a Getter.
+ Getter
+
+ // Set the given key/value in the cache, overwriting any existing value
+ // associated with that key. Keys may be at most 250 bytes in length.
+ //
+ // Returns:
+ // - nil on success
+ // - an implementation specific error otherwise
+ Set(key string, value interface{}, expires time.Duration) error
+
+ // Get the content associated multiple keys at once. On success, the caller
+ // may decode the values one at a time from the returned Getter.
+ //
+ // Returns:
+ // - the value getter, and a nil error if the operation completed.
+ // - an implementation specific error otherwise
+ GetMulti(keys ...string) (Getter, error)
+
+ // Delete the given key from the cache.
+ //
+ // Returns:
+ // - nil on a successful delete
+ // - ErrCacheMiss if the value was not in the cache
+ // - an implementation specific error otherwise
+ Delete(key string) error
+
+ // Add the given key/value to the cache ONLY IF the key does not already exist.
+ //
+ // Returns:
+ // - nil if the value was added to the cache
+ // - ErrNotStored if the key was already present in the cache
+ // - an implementation-specific error otherwise
+ Add(key string, value interface{}, expires time.Duration) error
+
+ // Set the given key/value in the cache ONLY IF the key already exists.
+ //
+ // Returns:
+ // - nil if the value was replaced
+ // - ErrNotStored if the key does not exist in the cache
+ // - an implementation specific error otherwise
+ Replace(key string, value interface{}, expires time.Duration) error
+
+ // Increment the value stored at the given key by the given amount.
+ // The value silently wraps around upon exceeding the uint64 range.
+ //
+ // Returns the new counter value if the operation was successful, or:
+ // - ErrCacheMiss if the key was not found in the cache
+ // - an implementation specific error otherwise
+ Increment(key string, n uint64) (newValue uint64, err error)
+
+ // Decrement the value stored at the given key by the given amount.
+ // The value is capped at 0 on underflow, with no error returned.
+ //
+ // Returns the new counter value if the operation was successful, or:
+ // - ErrCacheMiss if the key was not found in the cache
+ // - an implementation specific error otherwise
+ Decrement(key string, n uint64) (newValue uint64, err error)
+
+ // Expire all cache entries immediately.
+ // This is not implemented for the memcached cache (intentionally).
+ // Returns an implementation specific error if the operation failed.
+ Flush() error
+}
+
+var (
+ Instance Cache
+
+ ErrCacheMiss = errors.New("revel/cache: key not found")
+ ErrNotStored = errors.New("revel/cache: not stored")
+ ErrInvalidValue = errors.New("revel/cache: invalid value")
+)
+
+// The package implements the Cache interface (as sugar).
+
+func Get(key string, ptrValue interface{}) error { return Instance.Get(key, ptrValue) }
+func GetMulti(keys ...string) (Getter, error) { return Instance.GetMulti(keys...) }
+func Delete(key string) error { return Instance.Delete(key) }
+func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) }
+func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) }
+func Flush() error { return Instance.Flush() }
+func Set(key string, value interface{}, expires time.Duration) error {
+ return Instance.Set(key, value, expires)
+}
+func Add(key string, value interface{}, expires time.Duration) error {
+ return Instance.Add(key, value, expires)
+}
+func Replace(key string, value interface{}, expires time.Duration) error {
+ return Instance.Replace(key, value, expires)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+// Tests against a generic Cache interface.
+// They should pass for all implementations.
+type cacheFactory func(*testing.T, time.Duration) Cache
+
+// Test typical cache interactions
+func typicalGetSet(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ value := "foo"
+ if err = cache.Set("value", value, DefaultExpiryTime); err != nil {
+ t.Errorf("Error setting a value: %s", err)
+ }
+
+ value = ""
+ err = cache.Get("value", &value)
+ if err != nil {
+ t.Errorf("Error getting a value: %s", err)
+ }
+ if value != "foo" {
+ t.Errorf("Expected to get foo back, got %s", value)
+ }
+}
+
+// Test the increment-decrement cases
+func incrDecr(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ // Normal increment / decrement operation.
+ if err = cache.Set("int", 10, ForEverNeverExpiry); err != nil {
+ t.Errorf("Error setting int: %s", err)
+ }
+ time.Sleep(time.Second)
+ newValue, err := cache.Increment("int", 50)
+ if err != nil {
+ t.Errorf("Error incrementing int: %s", err)
+ }
+ if newValue != 60 {
+ t.Errorf("Expected 60, was %d", newValue)
+ }
+
+ if newValue, err = cache.Decrement("int", 50); err != nil {
+ t.Errorf("Error decrementing: %s", err)
+ }
+ if newValue != 10 {
+ t.Errorf("Expected 10, was %d", newValue)
+ }
+
+ // Increment wraparound
+ newValue, err = cache.Increment("int", math.MaxUint64-5)
+ if err != nil {
+ t.Errorf("Error wrapping around: %s", err)
+ }
+ if newValue != 4 {
+ t.Errorf("Expected wraparound 4, got %d", newValue)
+ }
+
+ // Decrement capped at 0
+ newValue, err = cache.Decrement("int", 25)
+ if err != nil {
+ t.Errorf("Error decrementing below 0: %s", err)
+ }
+ if newValue != 0 {
+ t.Errorf("Expected capped at 0, got %d", newValue)
+ }
+}
+
+func expiration(t *testing.T, newCache cacheFactory) {
+ // memcached does not support expiration times less than 1 second.
+ var err error
+ cache := newCache(t, time.Second)
+ // Test Set w/ DefaultExpiryTime
+ value := 10
+ if err = cache.Set("int", value, DefaultExpiryTime); err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ time.Sleep(2 * time.Second)
+ if err = cache.Get("int", &value); err != ErrCacheMiss {
+ t.Errorf("Expected CacheMiss, but got: %s", err)
+ }
+
+ // Test Set w/ short time
+ if err = cache.Set("int", value, time.Second); err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ time.Sleep(2 * time.Second)
+ if err = cache.Get("int", &value); err != ErrCacheMiss {
+ t.Errorf("Expected CacheMiss, but got: %s", err)
+ }
+
+ // Test Set w/ longer time.
+ if err = cache.Set("int", value, time.Hour); err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ time.Sleep(2 * time.Second)
+ if err = cache.Get("int", &value); err != nil {
+ t.Errorf("Expected to get the value, but got: %s", err)
+ }
+
+ // Test Set w/ forever.
+ if err = cache.Set("int", value, ForEverNeverExpiry); err != nil {
+ t.Errorf("Set failed: %s", err)
+ }
+ time.Sleep(2 * time.Second)
+ if err = cache.Get("int", &value); err != nil {
+ t.Errorf("Expected to get the value, but got: %s", err)
+ }
+}
+
+func emptyCache(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ err = cache.Get("notexist", 0)
+ if err == nil {
+ t.Errorf("Error expected for non-existent key")
+ }
+ if err != ErrCacheMiss {
+ t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
+ }
+
+ err = cache.Delete("notexist")
+ if err != ErrCacheMiss {
+ t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
+ }
+
+ _, err = cache.Increment("notexist", 1)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected cache miss incrementing non-existent key: %s", err)
+ }
+
+ _, err = cache.Decrement("notexist", 1)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected cache miss decrementing non-existent key: %s", err)
+ }
+}
+
+func testReplace(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ // Replace in an empty cache.
+ if err = cache.Replace("notexist", 1, ForEverNeverExpiry); err != ErrNotStored {
+ t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err)
+ }
+
+ // Set a value of 1, and replace it with 2
+ if err = cache.Set("int", 1, time.Second); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+
+ if err = cache.Replace("int", 2, time.Second); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+ var i int
+ if err = cache.Get("int", &i); err != nil {
+ t.Errorf("Unexpected error getting a replaced item: %s", err)
+ }
+ if i != 2 {
+ t.Errorf("Expected 2, got %d", i)
+ }
+
+ // Wait for it to expire and replace with 3 (unsuccessfully).
+ time.Sleep(2 * time.Second)
+ if err = cache.Replace("int", 3, time.Second); err != ErrNotStored {
+ t.Errorf("Expected ErrNotStored, got: %s", err)
+ }
+ if err = cache.Get("int", &i); err != ErrCacheMiss {
+ t.Errorf("Expected cache miss, got: %s", err)
+ }
+}
+
+func testAdd(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+ // Add to an empty cache.
+ if err = cache.Add("int", 1, time.Second*3); err != nil {
+ t.Errorf("Unexpected error adding to empty cache: %s", err)
+ }
+
+ // Try to add again. (fail)
+ if err = cache.Add("int", 2, time.Second*3); err != nil {
+ if err != ErrNotStored {
+ t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err)
+ }
+ }
+
+ // Wait for it to expire, and add again.
+ time.Sleep(8 * time.Second)
+ if err = cache.Add("int", 3, time.Second*5); err != nil {
+ t.Errorf("Unexpected error adding to cache: %s", err)
+ }
+
+ // Get and verify the value.
+ var i int
+ if err = cache.Get("int", &i); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+ if i != 3 {
+ t.Errorf("Expected 3, got: %d", i)
+ }
+}
+
+func testGetMulti(t *testing.T, newCache cacheFactory) {
+ cache := newCache(t, time.Hour)
+
+ m := map[string]interface{}{
+ "str": "foo",
+ "num": 42,
+ "foo": struct{ Bar string }{"baz"},
+ }
+
+ var keys []string
+ for key, value := range m {
+ keys = append(keys, key)
+ if err := cache.Set(key, value, time.Second*30); err != nil {
+ t.Errorf("Error setting a value: %s", err)
+ }
+ }
+
+ g, err := cache.GetMulti(keys...)
+ if err != nil {
+ t.Errorf("Error in get-multi: %s", err)
+ }
+
+ var str string
+ if err = g.Get("str", &str); err != nil || str != "foo" {
+ t.Errorf("Error getting str: %s / %s", err, str)
+ }
+
+ var num int
+ if err = g.Get("num", &num); err != nil || num != 42 {
+ t.Errorf("Error getting num: %s / %v", err, num)
+ }
+
+ var foo struct{ Bar string }
+ if err = g.Get("foo", &foo); err != nil || foo.Bar != "baz" {
+ t.Errorf("Error getting foo: %s / %v", err, foo)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "strings"
+ "time"
+
+ "github.com/revel/revel"
+)
+
+var cacheLog = revel.RevelLog.New("section", "cache")
+
+func init() {
+ revel.OnAppStart(func() {
+ // Set the default expiration time.
+ defaultExpiration := time.Hour // The default for the default is one hour.
+ if expireStr, found := revel.Config.String("cache.expires"); found {
+ var err error
+ if defaultExpiration, err = time.ParseDuration(expireStr); err != nil {
+ cacheLog.Panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error())
+ }
+ }
+
+ // make sure you aren't trying to use both memcached and redis
+ if revel.Config.BoolDefault("cache.memcached", false) && revel.Config.BoolDefault("cache.redis", false) {
+ cacheLog.Panic("You've configured both memcached and redis, please only include configuration for one cache!")
+ }
+
+ // Use memcached?
+ if revel.Config.BoolDefault("cache.memcached", false) {
+ hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
+ if len(hosts) == 0 {
+ cacheLog.Panic("Memcache enabled but no memcached hosts specified!")
+ }
+
+ Instance = NewMemcachedCache(hosts, defaultExpiration)
+ return
+ }
+
+ // Use Redis (share same config as memcached)?
+ if revel.Config.BoolDefault("cache.redis", false) {
+ hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
+ if len(hosts) == 0 {
+ cacheLog.Panic("Redis enabled but no Redis hosts specified!")
+ }
+ if len(hosts) > 1 {
+ cacheLog.Panic("Redis currently only supports one host!")
+ }
+ password := revel.Config.StringDefault("cache.redis.password", "")
+ Instance = NewRedisCache(hosts[0], password, defaultExpiration)
+ return
+ }
+
+ // By default, use the in-memory cache.
+ Instance = NewInMemoryCache(defaultExpiration)
+ })
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "fmt"
+ "reflect"
+ "time"
+
+ "github.com/patrickmn/go-cache"
+ "sync"
+)
+
+type InMemoryCache struct {
+ cache cache.Cache // Only expose the methods we want to make available
+ mu sync.RWMutex // For increment / decrement prevent reads and writes
+}
+
+func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache {
+ return InMemoryCache{cache: *cache.New(defaultExpiration, time.Minute), mu: sync.RWMutex{}}
+}
+
+func (c InMemoryCache) Get(key string, ptrValue interface{}) error {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ value, found := c.cache.Get(key)
+ if !found {
+ return ErrCacheMiss
+ }
+
+ v := reflect.ValueOf(ptrValue)
+ if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() {
+ v.Elem().Set(reflect.ValueOf(value))
+ return nil
+ }
+
+ err := fmt.Errorf("revel/cache: attempt to get %s, but can not set value %v", key, v)
+ cacheLog.Error(err.Error())
+ return err
+}
+
+func (c InMemoryCache) GetMulti(keys ...string) (Getter, error) {
+ return c, nil
+}
+
+func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ // NOTE: go-cache understands the values of DefaultExpiryTime and ForEverNeverExpiry
+ c.cache.Set(key, value, expires)
+ return nil
+}
+
+func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ err := c.cache.Add(key, value, expires)
+ if err != nil {
+ return ErrNotStored
+ }
+ return err
+}
+
+func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if err := c.cache.Replace(key, value, expires); err != nil {
+ return ErrNotStored
+ }
+ return nil
+}
+
+func (c InMemoryCache) Delete(key string) error {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ if _, found := c.cache.Get(key); !found {
+ return ErrCacheMiss
+ }
+ c.cache.Delete(key)
+ return nil
+}
+
+func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if _, found := c.cache.Get(key); !found {
+ return 0, ErrCacheMiss
+ }
+ if err = c.cache.Increment(key, int64(n)); err != nil {
+ return
+ }
+
+ return c.convertTypeToUint64(key)
+}
+
+func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if nv, err := c.convertTypeToUint64(key); err != nil {
+ return 0, err
+ } else {
+ // Stop from going below zero
+ if n > nv {
+ n = nv
+ }
+ }
+ if err = c.cache.Decrement(key, int64(n)); err != nil {
+ return
+ }
+
+ return c.convertTypeToUint64(key)
+}
+
+func (c InMemoryCache) Flush() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.cache.Flush()
+ return nil
+}
+
+// Fetches and returns the converted type to a uint64
+func (c InMemoryCache) convertTypeToUint64(key string) (newValue uint64, err error) {
+ v, found := c.cache.Get(key)
+ if !found {
+ return newValue, ErrCacheMiss
+ }
+
+ switch v.(type) {
+ case int:
+ newValue = uint64(v.(int))
+ case int8:
+ newValue = uint64(v.(int8))
+ case int16:
+ newValue = uint64(v.(int16))
+ case int32:
+ newValue = uint64(v.(int32))
+ case int64:
+ newValue = uint64(v.(int64))
+ case uint:
+ newValue = uint64(v.(uint))
+ case uintptr:
+ newValue = uint64(v.(uintptr))
+ case uint8:
+ newValue = uint64(v.(uint8))
+ case uint16:
+ newValue = uint64(v.(uint16))
+ case uint32:
+ newValue = uint64(v.(uint32))
+ case uint64:
+ newValue = uint64(v.(uint64))
+ case float32:
+ newValue = uint64(v.(float32))
+ case float64:
+ newValue = uint64(v.(float64))
+ default:
+ err = ErrInvalidValue
+ }
+ return
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "testing"
+ "time"
+)
+
+var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache {
+ return NewInMemoryCache(defaultExpiration)
+}
+
+// Test typical cache interactions
+func TestInMemoryCache_TypicalGetSet(t *testing.T) {
+ typicalGetSet(t, newInMemoryCache)
+}
+
+// Test the increment-decrement cases
+func TestInMemoryCache_IncrDecr(t *testing.T) {
+ incrDecr(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Expiration(t *testing.T) {
+ expiration(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_EmptyCache(t *testing.T) {
+ emptyCache(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Replace(t *testing.T) {
+ testReplace(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Add(t *testing.T) {
+ testAdd(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_GetMulti(t *testing.T) {
+ testGetMulti(t, newInMemoryCache)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "errors"
+ "time"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/revel/revel/logger"
+)
+
+// MemcachedCache wraps the Memcached client to meet the Cache interface.
+type MemcachedCache struct {
+ *memcache.Client
+ defaultExpiration time.Duration
+}
+
+func NewMemcachedCache(hostList []string, defaultExpiration time.Duration) MemcachedCache {
+ return MemcachedCache{memcache.New(hostList...), defaultExpiration}
+}
+
+func (c MemcachedCache) Set(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Set, key, value, expires)
+}
+
+func (c MemcachedCache) Add(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Add, key, value, expires)
+}
+
+func (c MemcachedCache) Replace(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Replace, key, value, expires)
+}
+
+func (c MemcachedCache) Get(key string, ptrValue interface{}) error {
+ item, err := c.Client.Get(key)
+ if err != nil {
+ return convertMemcacheError(err)
+ }
+ return Deserialize(item.Value, ptrValue)
+}
+
+func (c MemcachedCache) GetMulti(keys ...string) (Getter, error) {
+ items, err := c.Client.GetMulti(keys)
+ if err != nil {
+ return nil, convertMemcacheError(err)
+ }
+ return ItemMapGetter(items), nil
+}
+
+func (c MemcachedCache) Delete(key string) error {
+ return convertMemcacheError(c.Client.Delete(key))
+}
+
+func (c MemcachedCache) Increment(key string, delta uint64) (newValue uint64, err error) {
+ newValue, err = c.Client.Increment(key, delta)
+ return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
+ newValue, err = c.Client.Decrement(key, delta)
+ return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Flush() error {
+ err := errors.New("Flush: can not flush memcached")
+ cacheLog.Error(err.Error())
+ return err
+}
+
+func (c MemcachedCache) invoke(f func(*memcache.Client, *memcache.Item) error,
+ key string, value interface{}, expires time.Duration) error {
+
+ switch expires {
+ case DefaultExpiryTime:
+ expires = c.defaultExpiration
+ case ForEverNeverExpiry:
+ expires = time.Duration(0)
+ }
+
+ b, err := Serialize(value)
+ if err != nil {
+ return err
+ }
+ return convertMemcacheError(f(c.Client, &memcache.Item{
+ Key: key,
+ Value: b,
+ Expiration: int32(expires / time.Second),
+ }))
+}
+
+// ItemMapGetter implements a Getter on top of the returned item map.
+type ItemMapGetter map[string]*memcache.Item
+
+func (g ItemMapGetter) Get(key string, ptrValue interface{}) error {
+ item, ok := g[key]
+ if !ok {
+ return ErrCacheMiss
+ }
+
+ return Deserialize(item.Value, ptrValue)
+}
+
+func convertMemcacheError(err error) error {
+ switch err {
+ case nil:
+ return nil
+ case memcache.ErrCacheMiss:
+ return ErrCacheMiss
+ case memcache.ErrNotStored:
+ return ErrNotStored
+ }
+
+ cacheLog.Error("convertMemcacheError:", "error", err, "trace", logger.NewCallStack())
+ return err
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "net"
+ "testing"
+ "time"
+)
+
+// These tests require memcached running on localhost:11211 (the default)
+const testServer = "localhost:11211"
+
+var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache {
+ c, err := net.Dial("tcp", testServer)
+ if err == nil {
+ if _, err = c.Write([]byte("flush_all\r\n")); err != nil {
+ t.Errorf("Write failed: %s", err)
+ }
+ _ = c.Close()
+ return NewMemcachedCache([]string{testServer}, defaultExpiration)
+ }
+ t.Errorf("couldn't connect to memcached on %s", testServer)
+ t.FailNow()
+ panic("")
+}
+
+func TestMemcachedCache_TypicalGetSet(t *testing.T) {
+ typicalGetSet(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_IncrDecr(t *testing.T) {
+ incrDecr(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Expiration(t *testing.T) {
+ expiration(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_EmptyCache(t *testing.T) {
+ emptyCache(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Replace(t *testing.T) {
+ testReplace(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Add(t *testing.T) {
+ testAdd(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_GetMulti(t *testing.T) {
+ testGetMulti(t, newMemcachedCache)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "time"
+
+ "github.com/garyburd/redigo/redis"
+ "github.com/revel/revel"
+)
+
+// RedisCache wraps the Redis client to meet the Cache interface.
+type RedisCache struct {
+ pool *redis.Pool
+ defaultExpiration time.Duration
+}
+
+// NewRedisCache returns a new RedisCache with given parameters
+// until redigo supports sharding/clustering, only one host will be in hostList
+func NewRedisCache(host string, password string, defaultExpiration time.Duration) RedisCache {
+ var pool = &redis.Pool{
+ MaxIdle: revel.Config.IntDefault("cache.redis.maxidle", 5),
+ MaxActive: revel.Config.IntDefault("cache.redis.maxactive", 0),
+ IdleTimeout: time.Duration(revel.Config.IntDefault("cache.redis.idletimeout", 240)) * time.Second,
+ Dial: func() (redis.Conn, error) {
+ protocol := revel.Config.StringDefault("cache.redis.protocol", "tcp")
+ toc := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.connect", 10000))
+ tor := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.read", 5000))
+ tow := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.write", 5000))
+ c, err := redis.Dial(protocol, host,
+ redis.DialConnectTimeout(toc),
+ redis.DialReadTimeout(tor),
+ redis.DialWriteTimeout(tow))
+ if err != nil {
+ return nil, err
+ }
+ if len(password) > 0 {
+ if _, err = c.Do("AUTH", password); err != nil {
+ _ = c.Close()
+ return nil, err
+ }
+ } else {
+ // check with PING
+ if _, err = c.Do("PING"); err != nil {
+ _ = c.Close()
+ return nil, err
+ }
+ }
+ return c, err
+ },
+ // custom connection test method
+ TestOnBorrow: func(c redis.Conn, t time.Time) error {
+ _, err := c.Do("PING")
+ return err
+ },
+ }
+ return RedisCache{pool, defaultExpiration}
+}
+
+func (c RedisCache) Set(key string, value interface{}, expires time.Duration) error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ return c.invoke(conn.Do, key, value, expires)
+}
+
+func (c RedisCache) Add(key string, value interface{}, expires time.Duration) error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ existed, err := exists(conn, key)
+ if err != nil {
+ return err
+ } else if existed {
+ return ErrNotStored
+ }
+ return c.invoke(conn.Do, key, value, expires)
+}
+
+func (c RedisCache) Replace(key string, value interface{}, expires time.Duration) error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ existed, err := exists(conn, key)
+ if err != nil {
+ return err
+ } else if !existed {
+ return ErrNotStored
+ }
+
+ err = c.invoke(conn.Do, key, value, expires)
+ if value == nil {
+ return ErrNotStored
+ }
+ return err
+}
+
+func (c RedisCache) Get(key string, ptrValue interface{}) error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ raw, err := conn.Do("GET", key)
+ if err != nil {
+ return err
+ } else if raw == nil {
+ return ErrCacheMiss
+ }
+ item, err := redis.Bytes(raw, err)
+ if err != nil {
+ return err
+ }
+ return Deserialize(item, ptrValue)
+}
+
+func generalizeStringSlice(strs []string) []interface{} {
+ ret := make([]interface{}, len(strs))
+ for i, str := range strs {
+ ret[i] = str
+ }
+ return ret
+}
+
+func (c RedisCache) GetMulti(keys ...string) (Getter, error) {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ items, err := redis.Values(conn.Do("MGET", generalizeStringSlice(keys)...))
+ if err != nil {
+ return nil, err
+ } else if items == nil {
+ return nil, ErrCacheMiss
+ }
+
+ m := make(map[string][]byte)
+ for i, key := range keys {
+ m[key] = nil
+ if i < len(items) && items[i] != nil {
+ s, ok := items[i].([]byte)
+ if ok {
+ m[key] = s
+ }
+ }
+ }
+ return RedisItemMapGetter(m), nil
+}
+
+func exists(conn redis.Conn, key string) (bool, error) {
+ return redis.Bool(conn.Do("EXISTS", key))
+}
+
+func (c RedisCache) Delete(key string) error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ existed, err := redis.Bool(conn.Do("DEL", key))
+ if err == nil && !existed {
+ err = ErrCacheMiss
+ }
+ return err
+}
+
+func (c RedisCache) Increment(key string, delta uint64) (uint64, error) {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ // Check for existence *before* increment as per the cache contract.
+ // redis will auto create the key, and we don't want that. Since we need to do increment
+ // ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value
+ // and do the exists check this way to minimize calls to Redis
+ val, err := conn.Do("GET", key)
+ if err != nil {
+ return 0, err
+ } else if val == nil {
+ return 0, ErrCacheMiss
+ }
+ currentVal, err := redis.Int64(val, nil)
+ if err != nil {
+ return 0, err
+ }
+ sum := currentVal + int64(delta)
+ _, err = conn.Do("SET", key, sum)
+ if err != nil {
+ return 0, err
+ }
+ return uint64(sum), nil
+}
+
+func (c RedisCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ // Check for existence *before* increment as per the cache contract.
+ // redis will auto create the key, and we don't want that, hence the exists call
+ existed, err := exists(conn, key)
+ if err != nil {
+ return 0, err
+ } else if !existed {
+ return 0, ErrCacheMiss
+ }
+ // Decrement contract says you can only go to 0
+ // so we go fetch the value and if the delta is greater than the amount,
+ // 0 out the value
+ currentVal, err := redis.Int64(conn.Do("GET", key))
+ if err != nil {
+ return 0, err
+ }
+ if delta > uint64(currentVal) {
+ var tempint int64
+ tempint, err = redis.Int64(conn.Do("DECRBY", key, currentVal))
+ return uint64(tempint), err
+ }
+ tempint, err := redis.Int64(conn.Do("DECRBY", key, delta))
+ return uint64(tempint), err
+}
+
+func (c RedisCache) Flush() error {
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ _, err := conn.Do("FLUSHALL")
+ return err
+}
+
+func (c RedisCache) invoke(f func(string, ...interface{}) (interface{}, error),
+ key string, value interface{}, expires time.Duration) error {
+
+ switch expires {
+ case DefaultExpiryTime:
+ expires = c.defaultExpiration
+ case ForEverNeverExpiry:
+ expires = time.Duration(0)
+ }
+
+ b, err := Serialize(value)
+ if err != nil {
+ return err
+ }
+ conn := c.pool.Get()
+ defer func() {
+ _ = conn.Close()
+ }()
+ if expires > 0 {
+ _, err = f("SETEX", key, int32(expires/time.Second), b)
+ return err
+ }
+ _, err = f("SET", key, b)
+ return err
+}
+
+// RedisItemMapGetter implements a Getter on top of the returned item map.
+type RedisItemMapGetter map[string][]byte
+
+func (g RedisItemMapGetter) Get(key string, ptrValue interface{}) error {
+ item, ok := g[key]
+ if !ok {
+ return ErrCacheMiss
+ }
+ return Deserialize(item, ptrValue)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "net"
+ "testing"
+ "time"
+
+ "github.com/revel/config"
+ "github.com/revel/revel"
+)
+
+// These tests require redis server running on localhost:6379 (the default)
+const redisTestServer = "localhost:6379"
+
+var newRedisCache = func(t *testing.T, defaultExpiration time.Duration) Cache {
+ revel.Config = config.NewContext()
+
+ c, err := net.Dial("tcp", redisTestServer)
+ if err == nil {
+ if _, err = c.Write([]byte("flush_all\r\n")); err != nil {
+ t.Errorf("Write failed: %s", err)
+ }
+ _ = c.Close()
+
+ redisCache := NewRedisCache(redisTestServer, "", defaultExpiration)
+ if err = redisCache.Flush(); err != nil {
+ t.Errorf("Flush failed: %s", err)
+ }
+ return redisCache
+ }
+ t.Errorf("couldn't connect to redis on %s", redisTestServer)
+ t.FailNow()
+ panic("")
+}
+
+func TestRedisCache_TypicalGetSet(t *testing.T) {
+ typicalGetSet(t, newRedisCache)
+}
+
+func TestRedisCache_IncrDecr(t *testing.T) {
+ incrDecr(t, newRedisCache)
+}
+
+func TestRedisCache_Expiration(t *testing.T) {
+ expiration(t, newRedisCache)
+}
+
+func TestRedisCache_EmptyCache(t *testing.T) {
+ emptyCache(t, newRedisCache)
+}
+
+func TestRedisCache_Replace(t *testing.T) {
+ testReplace(t, newRedisCache)
+}
+
+func TestRedisCache_Add(t *testing.T) {
+ testAdd(t, newRedisCache)
+}
+
+func TestRedisCache_GetMulti(t *testing.T) {
+ testGetMulti(t, newRedisCache)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "bytes"
+ "encoding/gob"
+ "reflect"
+ "strconv"
+)
+
+// Serialize transforms the given value into bytes following these rules:
+// - If value is a byte array, it is returned as-is.
+// - If value is an int or uint type, it is returned as the ASCII representation
+// - Else, encoding/gob is used to serialize
+func Serialize(value interface{}) ([]byte, error) {
+ if data, ok := value.([]byte); ok {
+ return data, nil
+ }
+
+ switch v := reflect.ValueOf(value); v.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return []byte(strconv.FormatInt(v.Int(), 10)), nil
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return []byte(strconv.FormatUint(v.Uint(), 10)), nil
+ }
+
+ var b bytes.Buffer
+ encoder := gob.NewEncoder(&b)
+ if err := encoder.Encode(value); err != nil {
+ cacheLog.Error("Serialize: gob encoding failed", "value", value, "error", err)
+ return nil, err
+ }
+ return b.Bytes(), nil
+}
+
+// Deserialize transforms bytes produced by Serialize back into a Go object,
+// storing it into "ptr", which must be a pointer to the value type.
+func Deserialize(byt []byte, ptr interface{}) (err error) {
+ if data, ok := ptr.(*[]byte); ok {
+ *data = byt
+ return
+ }
+
+ if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr {
+ switch p := v.Elem(); p.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ var i int64
+ i, err = strconv.ParseInt(string(byt), 10, 64)
+ if err != nil {
+ cacheLog.Error("Deserialize: failed to parse int", "value", string(byt), "error", err)
+ } else {
+ p.SetInt(i)
+ }
+ return
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ var i uint64
+ i, err = strconv.ParseUint(string(byt), 10, 64)
+ if err != nil {
+ cacheLog.Error("Deserialize: failed to parse uint", "value", string(byt), "error", err)
+ } else {
+ p.SetUint(i)
+ }
+ return
+ }
+ }
+
+ b := bytes.NewBuffer(byt)
+ decoder := gob.NewDecoder(b)
+ if err = decoder.Decode(ptr); err != nil {
+ cacheLog.Error("Deserialize: glob decoding failed", "error", err)
+ return
+ }
+ return
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "reflect"
+ "testing"
+)
+
+type Struct1 struct {
+ X int
+}
+
+func (s Struct1) Method1() {}
+
+type Interface1 interface {
+ Method1()
+}
+
+var (
+ struct1 = Struct1{1}
+ ptrStruct = &Struct1{2}
+ emptyIface interface{} = Struct1{3}
+ iface1 Interface1 = Struct1{4}
+ sliceStruct = []Struct1{{5}, {6}, {7}}
+ ptrSliceStruct = []*Struct1{{8}, {9}, {10}}
+
+ valueMap = map[string]interface{}{
+ "bytes": []byte{0x61, 0x62, 0x63, 0x64},
+ "string": "string",
+ "bool": true,
+ "int": 5,
+ "int8": int8(5),
+ "int16": int16(5),
+ "int32": int32(5),
+ "int64": int64(5),
+ "uint": uint(5),
+ "uint8": uint8(5),
+ "uint16": uint16(5),
+ "uint32": uint32(5),
+ "uint64": uint64(5),
+ "float32": float32(5),
+ "float64": float64(5),
+ "array": [5]int{1, 2, 3, 4, 5},
+ "slice": []int{1, 2, 3, 4, 5},
+ "emptyIf": emptyIface,
+ "Iface1": iface1,
+ "map": map[string]string{"foo": "bar"},
+ "ptrStruct": ptrStruct,
+ "struct1": struct1,
+ "sliceStruct": sliceStruct,
+ "ptrSliceStruct": ptrSliceStruct,
+ }
+)
+
+// Test passing all kinds of data between serialize and deserialize.
+func TestRoundTrip(t *testing.T) {
+ for _, expected := range valueMap {
+ bytes, err := Serialize(expected)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ ptrActual := reflect.New(reflect.TypeOf(expected)).Interface()
+ err = Deserialize(bytes, ptrActual)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ actual := reflect.ValueOf(ptrActual).Elem().Interface()
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual)
+ }
+ }
+}
+
+func zeroMap(arg map[string]interface{}) map[string]interface{} {
+ result := map[string]interface{}{}
+ for key, value := range arg {
+ result[key] = reflect.Zero(reflect.TypeOf(value)).Interface()
+ }
+ return result
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "compress/gzip"
+ "compress/zlib"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+var compressionTypes = [...]string{
+ "gzip",
+ "deflate",
+}
+
+var compressableMimes = [...]string{
+ "text/plain",
+ "text/html",
+ "text/xml",
+ "text/css",
+ "application/json",
+ "application/xml",
+ "application/xhtml+xml",
+ "application/rss+xml",
+ "application/javascript",
+ "application/x-javascript",
+}
+
+// Local log instance for this class
+var compressLog = RevelLog.New("section", "compress")
+
+// WriteFlusher interface for compress writer
+type WriteFlusher interface {
+ io.Writer // An IO Writer
+ io.Closer // A closure
+ Flush() error /// A flush function
+}
+
+// The compressed writer
+type CompressResponseWriter struct {
+ Header *BufferedServerHeader // The header
+ ControllerResponse *Response // The response
+ OriginalWriter io.Writer // The writer
+ compressWriter WriteFlusher // The flushed writer
+ compressionType string // The compression type
+ headersWritten bool // True if written
+ closeNotify chan bool // The notify channel to close
+ parentNotify <-chan bool // The parent chanel to receive the closed event
+ closed bool // True if closed
+}
+
+// CompressFilter does compression of response body in gzip/deflate if
+// `results.compressed=true` in the app.conf
+func CompressFilter(c *Controller, fc []Filter) {
+ if c.Response.Out.internalHeader.Server != nil && Config.BoolDefault("results.compressed", false) {
+ if c.Response.Status != http.StatusNoContent && c.Response.Status != http.StatusNotModified {
+ if found, compressType, compressWriter := detectCompressionType(c.Request, c.Response); found {
+ writer := CompressResponseWriter{
+ ControllerResponse: c.Response,
+ OriginalWriter: c.Response.GetWriter(),
+ compressWriter: compressWriter,
+ compressionType: compressType,
+ headersWritten: false,
+ closeNotify: make(chan bool, 1),
+ closed: false,
+ }
+ // Swap out the header with our own
+ writer.Header = NewBufferedServerHeader(c.Response.Out.internalHeader.Server)
+ c.Response.Out.internalHeader.Server = writer.Header
+ if w, ok := c.Response.GetWriter().(http.CloseNotifier); ok {
+ writer.parentNotify = w.CloseNotify()
+ }
+ c.Response.SetWriter(&writer)
+ }
+ } else {
+ compressLog.Debug("CompressFilter: Compression disabled for response ", "status", c.Response.Status)
+ }
+ }
+ fc[0](c, fc[1:])
+}
+
+// Called to notify the writer is closing
+func (c CompressResponseWriter) CloseNotify() <-chan bool {
+ if c.parentNotify != nil {
+ return c.parentNotify
+ }
+ return c.closeNotify
+}
+
+// Cancel the writer
+func (c *CompressResponseWriter) cancel() {
+ c.closed = true
+}
+
+// Prepare the headers
+func (c *CompressResponseWriter) prepareHeaders() {
+ if c.compressionType != "" {
+ responseMime := ""
+ if t := c.Header.Get("Content-Type"); len(t) > 0 {
+ responseMime = t[0]
+ }
+ responseMime = strings.TrimSpace(strings.SplitN(responseMime, ";", 2)[0])
+ shouldEncode := false
+
+ if len(c.Header.Get("Content-Encoding")) == 0 {
+ for _, compressableMime := range compressableMimes {
+ if responseMime == compressableMime {
+ shouldEncode = true
+ c.Header.Set("Content-Encoding", c.compressionType)
+ c.Header.Del("Content-Length")
+ break
+ }
+ }
+ }
+
+ if !shouldEncode {
+ c.compressWriter = nil
+ c.compressionType = ""
+ }
+ }
+ c.Header.Release()
+}
+
+// Write the headers
+func (c *CompressResponseWriter) WriteHeader(status int) {
+ if c.closed {
+ return
+ }
+ c.headersWritten = true
+ c.prepareHeaders()
+ c.Header.SetStatus(status)
+}
+
+// Close the writer
+func (c *CompressResponseWriter) Close() error {
+ if c.closed {
+ return nil
+ }
+ if !c.headersWritten {
+ c.prepareHeaders()
+ }
+ if c.compressionType != "" {
+ c.Header.Del("Content-Length")
+ if err := c.compressWriter.Close(); err != nil {
+ // TODO When writing directly to stream, an error will be generated
+ compressLog.Error("Close: Error closing compress writer", "type", c.compressionType, "error", err)
+ }
+
+ }
+ // Non-blocking write to the closenotifier, if we for some reason should
+ // get called multiple times
+ select {
+ case c.closeNotify <- true:
+ default:
+ }
+ c.closed = true
+ return nil
+}
+
+// Write to the underling buffer
+func (c *CompressResponseWriter) Write(b []byte) (int, error) {
+ if c.closed {
+ return 0, io.ErrClosedPipe
+ }
+ // Abort if parent has been closed
+ if c.parentNotify != nil {
+ select {
+ case <-c.parentNotify:
+ return 0, io.ErrClosedPipe
+ default:
+ }
+ }
+ // Abort if we ourselves have been closed
+ if c.closed {
+ return 0, io.ErrClosedPipe
+ }
+
+ if !c.headersWritten {
+ c.prepareHeaders()
+ c.headersWritten = true
+ }
+ if c.compressionType != "" {
+ return c.compressWriter.Write(b)
+ }
+ return c.OriginalWriter.Write(b)
+}
+
+// DetectCompressionType method detects the compression type
+// from header "Accept-Encoding"
+func detectCompressionType(req *Request, resp *Response) (found bool, compressionType string, compressionKind WriteFlusher) {
+ if Config.BoolDefault("results.compressed", false) {
+ acceptedEncodings := strings.Split(req.GetHttpHeader("Accept-Encoding"), ",")
+
+ largestQ := 0.0
+ chosenEncoding := len(compressionTypes)
+
+ // I have fixed one edge case for issue #914
+ // But it's better to cover all possible edge cases or
+ // Adapt to https://github.com/golang/gddo/blob/master/httputil/header/header.go#L172
+ for _, encoding := range acceptedEncodings {
+ encoding = strings.TrimSpace(encoding)
+ encodingParts := strings.SplitN(encoding, ";", 2)
+
+ // If we are the format "gzip;q=0.8"
+ if len(encodingParts) > 1 {
+ q := strings.TrimSpace(encodingParts[1])
+ if len(q) == 0 || !strings.HasPrefix(q, "q=") {
+ continue
+ }
+
+ // Strip off the q=
+ num, err := strconv.ParseFloat(q[2:], 32)
+ if err != nil {
+ continue
+ }
+
+ if num >= largestQ && num > 0 {
+ if encodingParts[0] == "*" {
+ chosenEncoding = 0
+ largestQ = num
+ continue
+ }
+ for i, encoding := range compressionTypes {
+ if encoding == encodingParts[0] {
+ if i < chosenEncoding {
+ largestQ = num
+ chosenEncoding = i
+ }
+ break
+ }
+ }
+ }
+ } else {
+ // If we can accept anything, chose our preferred method.
+ if encodingParts[0] == "*" {
+ chosenEncoding = 0
+ largestQ = 1
+ break
+ }
+ // This is for just plain "gzip"
+ for i, encoding := range compressionTypes {
+ if encoding == encodingParts[0] {
+ if i < chosenEncoding {
+ largestQ = 1.0
+ chosenEncoding = i
+ }
+ break
+ }
+ }
+ }
+ }
+
+ if largestQ == 0 {
+ return
+ }
+
+ compressionType = compressionTypes[chosenEncoding]
+
+ switch compressionType {
+ case "gzip":
+ compressionKind = gzip.NewWriter(resp.GetWriter())
+ found = true
+ case "deflate":
+ compressionKind = zlib.NewWriter(resp.GetWriter())
+ found = true
+ }
+ }
+ return
+}
+
+// BufferedServerHeader will not send content out until the Released is called, from that point on it will act normally
+// It implements all the ServerHeader
+type BufferedServerHeader struct {
+ cookieList []string // The cookie list
+ headerMap map[string][]string // The header map
+ status int // The status
+ released bool // True if released
+ original ServerHeader // The original header
+}
+
+// Creates a new instance based on the ServerHeader
+func NewBufferedServerHeader(o ServerHeader) *BufferedServerHeader {
+ return &BufferedServerHeader{original: o, headerMap: map[string][]string{}}
+}
+
+// Sets the cookie
+func (bsh *BufferedServerHeader) SetCookie(cookie string) {
+ if bsh.released {
+ bsh.original.SetCookie(cookie)
+ } else {
+ bsh.cookieList = append(bsh.cookieList, cookie)
+ }
+}
+
+// Returns a cookie
+func (bsh *BufferedServerHeader) GetCookie(key string) (ServerCookie, error) {
+ return bsh.original.GetCookie(key)
+}
+
+// Sets (replace) the header key
+func (bsh *BufferedServerHeader) Set(key string, value string) {
+ if bsh.released {
+ bsh.original.Set(key, value)
+ } else {
+ bsh.headerMap[key] = []string{value}
+ }
+}
+
+// Add (append) to a key this value
+func (bsh *BufferedServerHeader) Add(key string, value string) {
+ if bsh.released {
+ bsh.original.Set(key, value)
+ } else {
+ old := []string{}
+ if v, found := bsh.headerMap[key]; found {
+ old = v
+ }
+ bsh.headerMap[key] = append(old, value)
+ }
+}
+
+// Delete this key
+func (bsh *BufferedServerHeader) Del(key string) {
+ if bsh.released {
+ bsh.original.Del(key)
+ } else {
+ delete(bsh.headerMap, key)
+ }
+}
+
+// Get this key
+func (bsh *BufferedServerHeader) Get(key string) (value []string) {
+ if bsh.released {
+ value = bsh.original.Get(key)
+ } else {
+ if v, found := bsh.headerMap[key]; found && len(v) > 0 {
+ value = v
+ } else {
+ value = bsh.original.Get(key)
+ }
+ }
+ return
+}
+
+// Get all header keys
+func (bsh *BufferedServerHeader) GetKeys() (value []string) {
+ if bsh.released {
+ value = bsh.original.GetKeys()
+ } else {
+ value = bsh.original.GetKeys()
+ for key := range bsh.headerMap {
+ found := false
+ for _,v := range value {
+ if v==key {
+ found = true
+ break
+ }
+ }
+ if !found {
+ value = append(value,key)
+ }
+ }
+ }
+ return
+}
+
+// Set the status
+func (bsh *BufferedServerHeader) SetStatus(statusCode int) {
+ if bsh.released {
+ bsh.original.SetStatus(statusCode)
+ } else {
+ bsh.status = statusCode
+ }
+}
+
+// Release the header and push the results to the original
+func (bsh *BufferedServerHeader) Release() {
+ bsh.released = true
+ for k, v := range bsh.headerMap {
+ for _, r := range v {
+ bsh.original.Set(k, r)
+ }
+ }
+ for _, c := range bsh.cookieList {
+ bsh.original.SetCookie(c)
+ }
+ if bsh.status > 0 {
+ bsh.original.SetStatus(bsh.status)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// Test that the render response is as expected.
+func TestBenchmarkCompressed(t *testing.T) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ t.Errorf("SetAction failed: %s", err)
+ }
+ Config.SetOption("results.compressed", "true")
+ result := Hotels{c}.Show(3)
+ result.Apply(c.Request, c.Response)
+ if !strings.Contains(resp.Body.String(), "300 Main St.") {
+ t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+ }
+}
+
+func BenchmarkRenderCompressed(b *testing.B) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ resp.Body = nil
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ b.Errorf("SetAction failed: %s", err)
+ }
+ Config.SetOption("results.compressed", "true")
+ b.ResetTimer()
+
+ hotels := Hotels{c}
+ for i := 0; i < b.N; i++ {
+ hotels.Show(3).Apply(c.Request, c.Response)
+ }
+}
+
+func BenchmarkRenderUnCompressed(b *testing.B) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ resp.Body = nil
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ b.Errorf("SetAction failed: %s", err)
+ }
+ Config.SetOption("results.compressed", "false")
+ b.ResetTimer()
+
+ hotels := Hotels{c}
+ for i := 0; i < b.N; i++ {
+ hotels.Show(3).Apply(c.Request, c.Response)
+ }
+}
--- /dev/null
+3dm=x-world/x-3dmf
+3dmf=x-world/x-3dmf
+7z=application/x-7z-compressed
+a=application/octet-stream
+aab=application/x-authorware-bin
+aam=application/x-authorware-map
+aas=application/x-authorware-seg
+abc=text/vndabc
+ace=application/x-ace-compressed
+acgi=text/html
+afl=video/animaflex
+ai=application/postscript
+aif=audio/aiff
+aifc=audio/aiff
+aiff=audio/aiff
+aim=application/x-aim
+aip=text/x-audiosoft-intra
+alz=application/x-alz-compressed
+ani=application/x-navi-animation
+aos=application/x-nokia-9000-communicator-add-on-software
+aps=application/mime
+arc=application/x-arc-compressed
+arj=application/arj
+art=image/x-jg
+asf=video/x-ms-asf
+asm=text/x-asm
+asp=text/asp
+asx=application/x-mplayer2
+au=audio/basic
+avi=video/x-msvideo
+avs=video/avs-video
+bcpio=application/x-bcpio
+bin=application/mac-binary
+bmp=image/bmp
+boo=application/book
+book=application/book
+boz=application/x-bzip2
+bsh=application/x-bsh
+bz2=application/x-bzip2
+bz=application/x-bzip
+c++=text/plain
+c=text/x-c
+cab=application/vnd.ms-cab-compressed
+cat=application/vndms-pkiseccat
+cc=text/x-c
+ccad=application/clariscad
+cco=application/x-cocoa
+cdf=application/cdf
+cer=application/pkix-cert
+cha=application/x-chat
+chat=application/x-chat
+chrt=application/vnd.kde.kchart
+class=application/java
+# ? class=application/java-vm
+com=text/plain
+conf=text/plain
+cpio=application/x-cpio
+cpp=text/x-c
+cpt=application/mac-compactpro
+crl=application/pkcs-crl
+crt=application/pkix-cert
+crx=application/x-chrome-extension
+csh=text/x-scriptcsh
+css=text/css
+csv=text/csv
+cxx=text/plain
+dar=application/x-dar
+dcr=application/x-director
+deb=application/x-debian-package
+deepv=application/x-deepv
+def=text/plain
+der=application/x-x509-ca-cert
+dif=video/x-dv
+dir=application/x-director
+divx=video/divx
+dl=video/dl
+dmg=application/x-apple-diskimage
+doc=application/msword
+dot=application/msword
+dp=application/commonground
+drw=application/drafting
+dump=application/octet-stream
+dv=video/x-dv
+dvi=application/x-dvi
+dwf=drawing/x-dwf=(old)
+dwg=application/acad
+dxf=application/dxf
+dxr=application/x-director
+el=text/x-scriptelisp
+elc=application/x-bytecodeelisp=(compiled=elisp)
+eml=message/rfc822
+env=application/x-envoy
+eps=application/postscript
+es=application/x-esrehber
+etx=text/x-setext
+evy=application/envoy
+exe=application/octet-stream
+f77=text/x-fortran
+f90=text/x-fortran
+f=text/x-fortran
+fdf=application/vndfdf
+fif=application/fractals
+fli=video/fli
+flo=image/florian
+flv=video/x-flv
+flx=text/vndfmiflexstor
+fmf=video/x-atomic3d-feature
+for=text/x-fortran
+fpx=image/vndfpx
+frl=application/freeloader
+funk=audio/make
+g3=image/g3fax
+g=text/plain
+gif=image/gif
+gl=video/gl
+gsd=audio/x-gsm
+gsm=audio/x-gsm
+gsp=application/x-gsp
+gss=application/x-gss
+gtar=application/x-gtar
+gz=application/x-compressed
+gzip=application/x-gzip
+h=text/x-h
+hdf=application/x-hdf
+help=application/x-helpfile
+hgl=application/vndhp-hpgl
+hh=text/x-h
+hlb=text/x-script
+hlp=application/hlp
+hpg=application/vndhp-hpgl
+hpgl=application/vndhp-hpgl
+hqx=application/binhex
+hta=application/hta
+htc=text/x-component
+htm=text/html
+html=text/html
+htmls=text/html
+htt=text/webviewhtml
+htx=text/html
+ice=x-conference/x-cooltalk
+ico=image/x-icon
+ics=text/calendar
+icz=text/calendar
+idc=text/plain
+ief=image/ief
+iefs=image/ief
+iges=application/iges
+igs=application/iges
+ima=application/x-ima
+imap=application/x-httpd-imap
+inf=application/inf
+ins=application/x-internett-signup
+ip=application/x-ip2
+isu=video/x-isvideo
+it=audio/it
+iv=application/x-inventor
+ivr=i-world/i-vrml
+ivy=application/x-livescreen
+jam=audio/x-jam
+jav=text/x-java-source
+java=text/x-java-source
+jcm=application/x-java-commerce
+jfif-tbnl=image/jpeg
+jfif=image/jpeg
+jnlp=application/x-java-jnlp-file
+jpe=image/jpeg
+jpeg=image/jpeg
+jpg=image/jpeg
+jps=image/x-jps
+js=application/javascript
+json=application/json
+jut=image/jutvision
+kar=audio/midi
+karbon=application/vnd.kde.karbon
+kfo=application/vnd.kde.kformula
+flw=application/vnd.kde.kivio
+kml=application/vnd.google-earth.kml+xml
+kmz=application/vnd.google-earth.kmz
+kon=application/vnd.kde.kontour
+kpr=application/vnd.kde.kpresenter
+kpt=application/vnd.kde.kpresenter
+ksp=application/vnd.kde.kspread
+kwd=application/vnd.kde.kword
+kwt=application/vnd.kde.kword
+ksh=text/x-scriptksh
+la=audio/nspaudio
+lam=audio/x-liveaudio
+latex=application/x-latex
+lha=application/lha
+lhx=application/octet-stream
+list=text/plain
+lma=audio/nspaudio
+log=text/plain
+lsp=text/x-scriptlisp
+lst=text/plain
+lsx=text/x-la-asf
+ltx=application/x-latex
+lzh=application/octet-stream
+lzx=application/lzx
+m1v=video/mpeg
+m2a=audio/mpeg
+m2v=video/mpeg
+m3u=audio/x-mpegurl
+m=text/x-m
+man=application/x-troff-man
+manifest=text/cache-manifest
+map=application/x-navimap
+mar=text/plain
+mbd=application/mbedlet
+mc$=application/x-magic-cap-package-10
+mcd=application/mcad
+mcf=text/mcf
+mcp=application/netmc
+me=application/x-troff-me
+mht=message/rfc822
+mhtml=message/rfc822
+mid=application/x-midi
+midi=application/x-midi
+mif=application/x-frame
+mime=message/rfc822
+mjf=audio/x-vndaudioexplosionmjuicemediafile
+mjpg=video/x-motion-jpeg
+mm=application/base64
+mme=application/base64
+mod=audio/mod
+moov=video/quicktime
+mov=video/quicktime
+movie=video/x-sgi-movie
+mp2=audio/mpeg
+mp3=audio/mpeg3
+mp4=video/mp4
+mpa=audio/mpeg
+mpc=application/x-project
+mpe=video/mpeg
+mpeg=video/mpeg
+mpg=video/mpeg
+mpga=audio/mpeg
+mpp=application/vndms-project
+mpt=application/x-project
+mpv=application/x-project
+mpx=application/x-project
+mrc=application/marc
+ms=application/x-troff-ms
+mv=video/x-sgi-movie
+my=audio/make
+mzz=application/x-vndaudioexplosionmzz
+nap=image/naplps
+naplps=image/naplps
+nc=application/x-netcdf
+ncm=application/vndnokiaconfiguration-message
+nif=image/x-niff
+niff=image/x-niff
+nix=application/x-mix-transfer
+nsc=application/x-conference
+nvd=application/x-navidoc
+o=application/octet-stream
+oda=application/oda
+odb=application/vnd.oasis.opendocument.database
+odc=application/vnd.oasis.opendocument.chart
+odf=application/vnd.oasis.opendocument.formula
+odg=application/vnd.oasis.opendocument.graphics
+odi=application/vnd.oasis.opendocument.image
+odm=application/vnd.oasis.opendocument.text-master
+odp=application/vnd.oasis.opendocument.presentation
+ods=application/vnd.oasis.opendocument.spreadsheet
+odt=application/vnd.oasis.opendocument.text
+oga=audio/ogg
+ogg=audio/ogg
+ogv=video/ogg
+omc=application/x-omc
+omcd=application/x-omcdatamaker
+omcr=application/x-omcregerator
+otc=application/vnd.oasis.opendocument.chart-template
+otf=application/vnd.oasis.opendocument.formula-template
+otg=application/vnd.oasis.opendocument.graphics-template
+oth=application/vnd.oasis.opendocument.text-web
+oti=application/vnd.oasis.opendocument.image-template
+otm=application/vnd.oasis.opendocument.text-master
+otp=application/vnd.oasis.opendocument.presentation-template
+ots=application/vnd.oasis.opendocument.spreadsheet-template
+ott=application/vnd.oasis.opendocument.text-template
+p10=application/pkcs10
+p12=application/pkcs-12
+p7a=application/x-pkcs7-signature
+p7c=application/pkcs7-mime
+p7m=application/pkcs7-mime
+p7r=application/x-pkcs7-certreqresp
+p7s=application/pkcs7-signature
+p=text/x-pascal
+part=application/pro_eng
+pas=text/pascal
+pbm=image/x-portable-bitmap
+pcl=application/vndhp-pcl
+pct=image/x-pict
+pcx=image/x-pcx
+pdb=chemical/x-pdb
+pdf=application/pdf
+pfunk=audio/make
+pgm=image/x-portable-graymap
+pic=image/pict
+pict=image/pict
+pkg=application/x-newton-compatible-pkg
+pko=application/vndms-pkipko
+pl=text/x-scriptperl
+plx=application/x-pixclscript
+pm4=application/x-pagemaker
+pm5=application/x-pagemaker
+pm=text/x-scriptperl-module
+png=image/png
+pnm=application/x-portable-anymap
+pot=application/mspowerpoint
+pov=model/x-pov
+ppa=application/vndms-powerpoint
+ppm=image/x-portable-pixmap
+pps=application/mspowerpoint
+ppt=application/mspowerpoint
+ppz=application/mspowerpoint
+pre=application/x-freelance
+prt=application/pro_eng
+ps=application/postscript
+psd=application/octet-stream
+pvu=paleovu/x-pv
+pwz=application/vndms-powerpoint
+py=text/x-scriptphyton
+pyc=applicaiton/x-bytecodepython
+qcp=audio/vndqcelp
+qd3=x-world/x-3dmf
+qd3d=x-world/x-3dmf
+qif=image/x-quicktime
+qt=video/quicktime
+qtc=video/x-qtc
+qti=image/x-quicktime
+qtif=image/x-quicktime
+ra=audio/x-pn-realaudio
+ram=audio/x-pn-realaudio
+rar=application/x-rar-compressed
+ras=application/x-cmu-raster
+rast=image/cmu-raster
+rexx=text/x-scriptrexx
+rf=image/vndrn-realflash
+rgb=image/x-rgb
+rm=application/vndrn-realmedia
+rmi=audio/mid
+rmm=audio/x-pn-realaudio
+rmp=audio/x-pn-realaudio
+rng=application/ringing-tones
+rnx=application/vndrn-realplayer
+roff=application/x-troff
+rp=image/vndrn-realpix
+rpm=audio/x-pn-realaudio-plugin
+rt=text/vndrn-realtext
+rtf=text/richtext
+rtx=text/richtext
+rv=video/vndrn-realvideo
+s=text/x-asm
+s3m=audio/s3m
+s7z=application/x-7z-compressed
+saveme=application/octet-stream
+sbk=application/x-tbook
+scm=text/x-scriptscheme
+sdml=text/plain
+sdp=application/sdp
+sdr=application/sounder
+sea=application/sea
+set=application/set
+sgm=text/x-sgml
+sgml=text/x-sgml
+sh=text/x-scriptsh
+shar=application/x-bsh
+shtml=text/x-server-parsed-html
+sid=audio/x-psid
+skd=application/x-koan
+skm=application/x-koan
+skp=application/x-koan
+skt=application/x-koan
+sit=application/x-stuffit
+sitx=application/x-stuffitx
+sl=application/x-seelogo
+smi=application/smil
+smil=application/smil
+snd=audio/basic
+sol=application/solids
+spc=text/x-speech
+spl=application/futuresplash
+spr=application/x-sprite
+sprite=application/x-sprite
+spx=audio/ogg
+src=application/x-wais-source
+ssi=text/x-server-parsed-html
+ssm=application/streamingmedia
+sst=application/vndms-pkicertstore
+step=application/step
+stl=application/sla
+stp=application/step
+sv4cpio=application/x-sv4cpio
+sv4crc=application/x-sv4crc
+svf=image/vnddwg
+svg=image/svg+xml
+svr=application/x-world
+swf=application/x-shockwave-flash
+t=application/x-troff
+talk=text/x-speech
+tar=application/x-tar
+tbk=application/toolbook
+tcl=text/x-scripttcl
+tcsh=text/x-scripttcsh
+tex=application/x-tex
+texi=application/x-texinfo
+texinfo=application/x-texinfo
+text=text/plain
+tgz=application/gnutar
+tif=image/tiff
+tiff=image/tiff
+tr=application/x-troff
+tsi=audio/tsp-audio
+tsp=application/dsptype
+tsv=text/tab-separated-values
+turbot=image/florian
+txt=text/plain
+uil=text/x-uil
+uni=text/uri-list
+unis=text/uri-list
+unv=application/i-deas
+uri=text/uri-list
+uris=text/uri-list
+ustar=application/x-ustar
+uu=text/x-uuencode
+uue=text/x-uuencode
+vcd=application/x-cdlink
+vcf=text/x-vcard
+vcard=text/x-vcard
+vcs=text/x-vcalendar
+vda=application/vda
+vdo=video/vdo
+vew=application/groupwise
+viv=video/vivo
+vivo=video/vivo
+vmd=application/vocaltec-media-desc
+vmf=application/vocaltec-media-file
+voc=audio/voc
+vos=video/vosaic
+vox=audio/voxware
+vqe=audio/x-twinvq-plugin
+vqf=audio/x-twinvq
+vql=audio/x-twinvq-plugin
+vrml=application/x-vrml
+vrt=x-world/x-vrt
+vsd=application/x-visio
+vst=application/x-visio
+vsw=application/x-visio
+w60=application/wordperfect60
+w61=application/wordperfect61
+w6w=application/msword
+wav=audio/wav
+wb1=application/x-qpro
+wbmp=image/vnd.wap.wbmp
+web=application/vndxara
+wiz=application/msword
+wk1=application/x-123
+wmf=windows/metafile
+wml=text/vnd.wap.wml
+wmlc=application/vnd.wap.wmlc
+wmls=text/vnd.wap.wmlscript
+wmlsc=application/vnd.wap.wmlscriptc
+word=application/msword
+wp5=application/wordperfect
+wp6=application/wordperfect
+wp=application/wordperfect
+wpd=application/wordperfect
+wq1=application/x-lotus
+wri=application/mswrite
+wrl=application/x-world
+wrz=model/vrml
+wsc=text/scriplet
+wsrc=application/x-wais-source
+wtk=application/x-wintalk
+x-png=image/png
+xbm=image/x-xbitmap
+xdr=video/x-amt-demorun
+xgz=xgl/drawing
+xif=image/vndxiff
+xl=application/excel
+xla=application/excel
+xlb=application/excel
+xlc=application/excel
+xld=application/excel
+xlk=application/excel
+xll=application/excel
+xlm=application/excel
+xls=application/excel
+xlt=application/excel
+xlv=application/excel
+xlw=application/excel
+xm=audio/xm
+xml=text/xml
+xmz=xgl/movie
+xpix=application/x-vndls-xpix
+xpm=image/x-xpixmap
+xsr=video/x-amt-showrun
+xwd=image/x-xwd
+xyz=chemical/x-pdb
+z=application/x-compress
+zip=application/zip
+zoo=application/octet-stream
+zsh=text/x-scriptzsh
+# Office 2007 mess - http://wdg.uncc.edu/Microsoft_Office_2007_MIME_Types_for_Apache_and_IIS
+docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document
+docm=application/vnd.ms-word.document.macroEnabled.12
+dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template
+dotm=application/vnd.ms-word.template.macroEnabled.12
+xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+xlsm=application/vnd.ms-excel.sheet.macroEnabled.12
+xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template
+xltm=application/vnd.ms-excel.template.macroEnabled.12
+xlsb=application/vnd.ms-excel.sheet.binary.macroEnabled.12
+xlam=application/vnd.ms-excel.addin.macroEnabled.12
+pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation
+pptm=application/vnd.ms-powerpoint.presentation.macroEnabled.12
+ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow
+ppsm=application/vnd.ms-powerpoint.slideshow.macroEnabled.12
+potx=application/vnd.openxmlformats-officedocument.presentationml.template
+potm=application/vnd.ms-powerpoint.template.macroEnabled.12
+ppam=application/vnd.ms-powerpoint.addin.macroEnabled.12
+sldx=application/vnd.openxmlformats-officedocument.presentationml.slide
+sldm=application/vnd.ms-powerpoint.slide.macroEnabled.12
+thmx=application/vnd.ms-officetheme
+onetoc=application/onenote
+onetoc2=application/onenote
+onetmp=application/onenote
+onepkg=application/onenote
+# koffice
+
+# iWork
+key=application/x-iwork-keynote-sffkey
+kth=application/x-iwork-keynote-sffkth
+nmbtemplate=application/x-iwork-numbers-sfftemplate
+numbers=application/x-iwork-numbers-sffnumbers
+pages=application/x-iwork-pages-sffpages
+template=application/x-iwork-pages-sfftemplate
+
+# Extensions for Mozilla apps (Firefox and friends)
+xpi=application/x-xpinstall
+
+# Opera extensions
+oex=application/x-opera-extension
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/revel/revel/logger"
+ "github.com/revel/revel/session"
+ "github.com/revel/revel/utils"
+)
+
+// Controller Revel's controller structure that gets embedded in user defined
+// controllers
+type Controller struct {
+ Name string // The controller name, e.g. "Application"
+ Type *ControllerType // A description of the controller type.
+ MethodName string // The method name, e.g. "Index"
+ MethodType *MethodType // A description of the invoked action type.
+ AppController interface{} // The controller that was instantiated. embeds revel.Controller
+ Action string // The fully qualified action name, e.g. "App.Index"
+ ClientIP string // holds IP address of request came from
+
+ Request *Request
+ Response *Response
+ Result Result
+
+ Flash Flash // User cookie, cleared after 1 request.
+ Session session.Session // Session, stored using the session engine specified
+ Params *Params // Parameters from URL and form (including multipart).
+ Args map[string]interface{} // Per-request scratch space.
+ ViewArgs map[string]interface{} // Variables passed to the template.
+ Validation *Validation // Data validation helpers
+ Log logger.MultiLogger // Context Logger
+}
+
+// The map of controllers, controllers are mapped by using the namespace|controller_name as the key
+var controllers = make(map[string]*ControllerType)
+var controllerLog = RevelLog.New("section", "controller")
+
+// NewController returns new controller instance for Request and Response
+func NewControllerEmpty() *Controller {
+ return &Controller{Request: NewRequest(nil), Response: NewResponse(nil)}
+}
+
+// New controller, creates a new instance wrapping the request and response in it
+func NewController(context ServerContext) *Controller {
+ c := NewControllerEmpty()
+ c.SetController(context)
+ return c
+}
+
+// Sets the request and the response for the controller
+func (c *Controller) SetController(context ServerContext) {
+
+ c.Request.SetRequest(context.GetRequest())
+ c.Response.SetResponse(context.GetResponse())
+ c.Request.controller = c
+ c.Params = new(Params)
+ c.Args = map[string]interface{}{}
+ c.ViewArgs = map[string]interface{}{
+ "RunMode": RunMode,
+ "DevMode": DevMode,
+ }
+
+}
+func (c *Controller) Destroy() {
+ // When the instantiated controller gets injected
+ // It inherits this method, so we need to
+ // check to see if the controller is nil before performing
+ // any actions
+ if c == nil {
+ return
+ }
+ if c.AppController != nil {
+ c.resetAppControllerFields()
+ // Return this instance to the pool
+ appController := c.AppController
+ c.AppController = nil
+ if RevelConfig.Controller.Reuse {
+ RevelConfig.Controller.CachedMap[c.Name].Push(appController)
+ }
+ c.AppController = nil
+ }
+
+ c.Request.Destroy()
+ c.Response.Destroy()
+ c.Params = nil
+ c.Args = nil
+ c.ViewArgs = nil
+ c.Name = ""
+ c.Type = nil
+ c.MethodName = ""
+ c.MethodType = nil
+ c.Action = ""
+ c.ClientIP = ""
+ c.Result = nil
+ c.Flash = Flash{}
+ c.Session = session.NewSession()
+ c.Params = nil
+ c.Validation = nil
+ c.Log = nil
+}
+
+// FlashParams serializes the contents of Controller.Params to the Flash
+// cookie.
+func (c *Controller) FlashParams() {
+ for key, vals := range c.Params.Values {
+ c.Flash.Out[key] = strings.Join(vals, ",")
+ }
+}
+
+func (c *Controller) SetCookie(cookie *http.Cookie) {
+ c.Response.Out.internalHeader.SetCookie(cookie.String())
+}
+
+type ErrorCoder interface {
+ HTTPCode() int
+}
+
+func (c *Controller) RenderError(err error) Result {
+ if coder, ok := err.(ErrorCoder); ok {
+ c.setStatusIfNil(coder.HTTPCode())
+ } else {
+ c.setStatusIfNil(http.StatusInternalServerError)
+ }
+
+ return ErrorResult{c.ViewArgs, err}
+}
+
+func (c *Controller) setStatusIfNil(status int) {
+ if c.Response.Status == 0 {
+ c.Response.Status = status
+ }
+}
+
+// Render a template corresponding to the calling Controller method.
+// Arguments will be added to c.ViewArgs prior to rendering the template.
+// They are keyed on their local identifier.
+//
+// For example:
+//
+// func (c Users) ShowUser(id int) revel.Result {
+// user := loadUser(id)
+// return c.Render(user)
+// }
+//
+// This action will render views/Users/ShowUser.html, passing in an extra
+// key-value "user": (User).
+//
+// This is the slower magical version which uses the runtime
+// to determine
+// 1) Set c.ViewArgs to the arguments passed into this function
+// 2) How to call the RenderTemplate by building the following line
+// c.RenderTemplate(c.Name + "/" + c.MethodType.Name + "." + c.Request.Format)
+//
+// If you want your code to run faster it is recommended you add the template values directly
+// to the c.ViewArgs and call c.RenderTemplate directly
+func (c *Controller) Render(extraViewArgs ...interface{}) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ // Get the calling function line number.
+ _, _, line, ok := runtime.Caller(1)
+ if !ok {
+ controllerLog.Error("Render: Failed to get Caller information")
+ }
+
+ // Get the extra ViewArgs passed in.
+ if renderArgNames, ok := c.MethodType.RenderArgNames[line]; ok {
+ if len(renderArgNames) == len(extraViewArgs) {
+ for i, extraRenderArg := range extraViewArgs {
+ c.ViewArgs[renderArgNames[i]] = extraRenderArg
+ }
+ } else {
+ controllerLog.Error(fmt.Sprint(len(renderArgNames), "RenderArg names found for",
+ len(extraViewArgs), "extra ViewArgs"))
+ }
+ } else {
+ controllerLog.Error(fmt.Sprint("No RenderArg names found for Render call on line", line,
+ "(Action", c.Action, ")"),"stack",logger.NewCallStack())
+ }
+
+ return c.RenderTemplate(c.Name + "/" + c.MethodType.Name + "." + c.Request.Format)
+}
+
+// RenderTemplate method does less magical way to render a template.
+// Renders the given template, using the current ViewArgs.
+func (c *Controller) RenderTemplate(templatePath string) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ // Get the Template.
+ lang, _ := c.ViewArgs[CurrentLocaleViewArg].(string)
+ template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+ if err != nil {
+ return c.RenderError(err)
+ }
+
+ return &RenderTemplateResult{
+ Template: template,
+ ViewArgs: c.ViewArgs,
+ }
+}
+
+// TemplateOutput returns the result of the template rendered using the controllers ViewArgs.
+func (c *Controller) TemplateOutput(templatePath string) (data []byte, err error) {
+ return TemplateOutputArgs(templatePath, c.ViewArgs)
+}
+
+// RenderJSON uses encoding/json.Marshal to return JSON to the client.
+func (c *Controller) RenderJSON(o interface{}) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ return RenderJSONResult{o, ""}
+}
+
+// RenderJSONP renders JSONP result using encoding/json.Marshal
+func (c *Controller) RenderJSONP(callback string, o interface{}) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ return RenderJSONResult{o, callback}
+}
+
+// RenderXML uses encoding/xml.Marshal to return XML to the client.
+func (c *Controller) RenderXML(o interface{}) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ return RenderXMLResult{o}
+}
+
+// RenderText renders plaintext in response, printf style.
+func (c *Controller) RenderText(text string, objs ...interface{}) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ finalText := text
+ if len(objs) > 0 {
+ finalText = fmt.Sprintf(text, objs...)
+ }
+ return &RenderTextResult{finalText}
+}
+
+// RenderHTML renders html in response
+func (c *Controller) RenderHTML(html string) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ return &RenderHTMLResult{html}
+}
+
+// Todo returns an HTTP 501 Not Implemented "todo" indicating that the
+// action isn't done yet.
+func (c *Controller) Todo() Result {
+ c.Response.Status = http.StatusNotImplemented
+ controllerLog.Debug("Todo: Not implemented function", "action", c.Action)
+ return c.RenderError(&Error{
+ Title: "TODO",
+ Description: "This action is not implemented",
+ })
+}
+
+// NotFound returns an HTTP 404 Not Found response whose body is the
+// formatted string of msg and objs.
+func (c *Controller) NotFound(msg string, objs ...interface{}) Result {
+ finalText := msg
+ if len(objs) > 0 {
+ finalText = fmt.Sprintf(msg, objs...)
+ }
+ c.Response.Status = http.StatusNotFound
+ return c.RenderError(&Error{
+ Title: "Not Found",
+ Description: finalText,
+ })
+}
+
+// Forbidden returns an HTTP 403 Forbidden response whose body is the
+// formatted string of msg and objs.
+func (c *Controller) Forbidden(msg string, objs ...interface{}) Result {
+ finalText := msg
+ if len(objs) > 0 {
+ finalText = fmt.Sprintf(msg, objs...)
+ }
+ c.Response.Status = http.StatusForbidden
+ return c.RenderError(&Error{
+ Title: "Forbidden",
+ Description: finalText,
+ })
+}
+
+// RenderFileName returns a file indicated by the path as provided via the filename.
+// It can be either displayed inline or downloaded as an attachment.
+// The name and size are taken from the file info.
+func (c *Controller) RenderFileName(filename string, delivery ContentDisposition) Result {
+ f, err := os.Open(filename)
+ if err != nil {
+ c.Log.Errorf("Cant open file: %v", err)
+ return c.RenderError(err)
+ }
+ return c.RenderFile(f, delivery)
+}
+
+// RenderFile returns a file, either displayed inline or downloaded
+// as an attachment. The name and size are taken from the file info.
+func (c *Controller) RenderFile(file *os.File, delivery ContentDisposition) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ var (
+ modtime = time.Now()
+ fileInfo, err = file.Stat()
+ )
+ if err != nil {
+ controllerLog.Error("RenderFile: error", "error", err)
+ }
+ if fileInfo != nil {
+ modtime = fileInfo.ModTime()
+ }
+ return c.RenderBinary(file, filepath.Base(file.Name()), delivery, modtime)
+}
+
+// RenderBinary is like RenderFile() except that it instead of a file on disk,
+// it renders data from memory (which could be a file that has not been written,
+// the output from some function, or bytes streamed from somewhere else, as long
+// it implements io.Reader). When called directly on something generated or
+// streamed, modtime should mostly likely be time.Now().
+func (c *Controller) RenderBinary(memfile io.Reader, filename string, delivery ContentDisposition, modtime time.Time) Result {
+ c.setStatusIfNil(http.StatusOK)
+
+ return &BinaryResult{
+ Reader: memfile,
+ Name: filename,
+ Delivery: delivery,
+ Length: -1, // http.ServeContent gets the length itself unless memfile is a stream.
+ ModTime: modtime,
+ }
+}
+
+// Redirect to an action or to a URL.
+// c.Redirect(Controller.Action)
+// c.Redirect("/controller/action")
+// c.Redirect("/controller/%d/action", id)
+func (c *Controller) Redirect(val interface{}, args ...interface{}) Result {
+ c.setStatusIfNil(http.StatusFound)
+
+ if url, ok := val.(string); ok {
+ if len(args) == 0 {
+ return &RedirectToURLResult{url}
+ }
+ return &RedirectToURLResult{fmt.Sprintf(url, args...)}
+ }
+ return &RedirectToActionResult{val, args}
+}
+
+// This stats returns some interesting stats based on what is cached in memory
+// and what is available directly
+func (c *Controller) Stats() map[string]interface{} {
+ result := CurrentEngine.Stats()
+ if RevelConfig.Controller.Reuse {
+ result["revel-controllers"] = RevelConfig.Controller.Stack.String()
+ for key, appStack := range RevelConfig.Controller.CachedMap {
+ result["app-" + key] = appStack.String()
+ }
+ }
+ return result
+}
+
+// Message performs a lookup for the given message name using the given
+// arguments using the current language defined for this controller.
+//
+// The current language is set by the i18n plugin.
+func (c *Controller) Message(message string, args ...interface{}) string {
+ return MessageFunc(c.Request.Locale, message, args...)
+}
+
+// SetAction sets the action that is being invoked in the current request.
+// It sets the following properties: Name, Action, Type, MethodType
+func (c *Controller) SetAction(controllerName, methodName string) error {
+
+ return c.SetTypeAction(controllerName, methodName, nil)
+}
+
+// SetAction sets the assigns the Controller type, sets the action and initializes the controller
+func (c *Controller) SetTypeAction(controllerName, methodName string, typeOfController *ControllerType) error {
+
+ // Look up the controller and method types.
+ if typeOfController == nil {
+ if c.Type = ControllerTypeByName(controllerName, anyModule); c.Type == nil {
+ return errors.New("revel/controller: failed to find controller " + controllerName)
+ }
+ } else {
+ c.Type = typeOfController
+ }
+
+ // Note method name is case insensitive search
+ if c.MethodType = c.Type.Method(methodName); c.MethodType == nil {
+ return errors.New("revel/controller: failed to find action " + controllerName + "." + methodName)
+ }
+
+ c.Name, c.MethodName = c.Type.Type.Name(), c.MethodType.Name
+ c.Action = c.Name + "." + c.MethodName
+
+ // Update Logger with controller and namespace
+ if c.Log != nil {
+ c.Log = c.Log.New("action", c.Action, "namespace", c.Type.Namespace)
+ }
+
+ if RevelConfig.Controller.Reuse {
+ if _, ok := RevelConfig.Controller.CachedMap[c.Name]; !ok {
+ // Create a new stack for this controller
+ localType := c.Type.Type
+ RevelConfig.Controller.CachedMap[c.Name] = utils.NewStackLock(
+ RevelConfig.Controller.CachedStackSize,
+ RevelConfig.Controller.CachedStackMaxSize,
+ func() interface{} {
+ return reflect.New(localType).Interface()
+ })
+ }
+ // Instantiate the controller.
+ c.AppController = RevelConfig.Controller.CachedMap[c.Name].Pop()
+ } else {
+ c.AppController = reflect.New(c.Type.Type).Interface()
+ }
+ c.setAppControllerFields()
+
+ return nil
+}
+
+func ControllerTypeByName(controllerName string, moduleSource *Module) (c *ControllerType) {
+ var found bool
+ if c, found = controllers[controllerName]; !found {
+ // Backup, passed in controllerName should be in lower case, but may not be
+ if c, found = controllers[strings.ToLower(controllerName)]; !found {
+ controllerLog.Debug("ControllerTypeByName: Cannot find controller in controllers map ", "controller", controllerName)
+ // Search for the controller by name
+ for _, cType := range controllers {
+ testControllerName := strings.ToLower(cType.Type.Name())
+ if testControllerName == strings.ToLower(controllerName) && (cType.ModuleSource == moduleSource || moduleSource == anyModule) {
+ controllerLog.Warn("ControllerTypeByName: Matched empty namespace controller ", "controller", controllerName, "namespace", cType.ModuleSource.Name)
+ c = cType
+ found = true
+ break
+ }
+ }
+ }
+ }
+ return
+}
+
+// Injects this instance (c) into the AppController instance
+func (c *Controller) setAppControllerFields() {
+ appController := reflect.ValueOf(c.AppController).Elem()
+ cValue := reflect.ValueOf(c)
+ for _, index := range c.Type.ControllerIndexes {
+ appController.FieldByIndex(index).Set(cValue)
+ }
+}
+
+// Removes this instance (c) from the AppController instance
+func (c *Controller) resetAppControllerFields() {
+ appController := reflect.ValueOf(c.AppController).Elem()
+ // Zero out controller
+ for _, index := range c.Type.ControllerIndexes {
+ appController.FieldByIndex(index).Set(reflect.Zero(reflect.TypeOf(c.AppController).Elem().FieldByIndex(index).Type))
+ }
+}
+
+func findControllers(appControllerType reflect.Type) (indexes [][]int) {
+ // It might be a multi-level embedding. To find the controllers, we follow
+ // every anonymous field, using breadth-first search.
+ type nodeType struct {
+ val reflect.Value
+ index []int
+ }
+ appControllerPtr := reflect.New(appControllerType)
+ queue := []nodeType{{appControllerPtr, []int{}}}
+ for len(queue) > 0 {
+ // Get the next value and de-reference it if necessary.
+ var (
+ node = queue[0]
+ elem = node.val
+ elemType = elem.Type()
+ )
+ if elemType.Kind() == reflect.Ptr {
+ elem = elem.Elem()
+ elemType = elem.Type()
+ }
+ queue = queue[1:]
+
+ // #944 if the type's Kind is not `Struct` move on,
+ // otherwise `elem.NumField()` will panic
+ if elemType.Kind() != reflect.Struct {
+ continue
+ }
+
+ // Look at all the struct fields.
+ for i := 0; i < elem.NumField(); i++ {
+ // If this is not an anonymous field, skip it.
+ structField := elemType.Field(i)
+ if !structField.Anonymous {
+ continue
+ }
+
+ fieldValue := elem.Field(i)
+ fieldType := structField.Type
+
+ // If it's a Controller, record the field indexes to get here.
+ if fieldType == controllerPtrType {
+ indexes = append(indexes, append(node.index, i))
+ continue
+ }
+
+ queue = append(queue,
+ nodeType{fieldValue, append(append([]int{}, node.index...), i)})
+ }
+ }
+ return
+}
+
+// RegisterController registers a Controller and its Methods with Revel.
+func RegisterController(c interface{}, methods []*MethodType) {
+ // De-star the controller type
+ // (e.g. given TypeOf((*Application)(nil)), want TypeOf(Application))
+ elem := reflect.TypeOf(c).Elem()
+
+ // De-star all of the method arg types too.
+ for _, m := range methods {
+ m.lowerName = strings.ToLower(m.Name)
+ for _, arg := range m.Args {
+ arg.Type = arg.Type.Elem()
+ }
+ }
+
+ // Fetch module for controller, if none found controller must be part of the app
+ controllerModule := ModuleFromPath(elem.PkgPath(), true)
+
+ controllerType := AddControllerType(controllerModule, elem, methods)
+
+ controllerLog.Debug("RegisterController:Registered controller", "controller", controllerType.Name())
+}
--- /dev/null
+package revel
+
+import (
+ "reflect"
+ "strings"
+)
+
+// Controller registry and types.
+type ControllerType struct {
+ Namespace string // The namespace of the controller
+ ModuleSource *Module // The module for the controller
+ Type reflect.Type
+ Methods []*MethodType
+ ControllerIndexes [][]int // FieldByIndex to all embedded *Controllers
+ ControllerEvents *ControllerTypeEvents
+}
+type ControllerTypeEvents struct {
+ Before, After, Finally, Panic []*ControllerFieldPath
+}
+
+// The controller field path provides the caller the ability to invoke the call
+// directly
+type ControllerFieldPath struct {
+ IsPointer bool
+ FieldIndexPath []int
+ FunctionCall reflect.Value
+}
+
+type MethodType struct {
+ Name string
+ Args []*MethodArg
+ RenderArgNames map[int][]string
+ lowerName string
+ Index int
+}
+
+type MethodArg struct {
+ Name string
+ Type reflect.Type
+}
+
+// Adds the controller to the controllers map using its namespace, also adds it to the module list of controllers.
+// If the controller is in the main application it is added without its namespace as well.
+func AddControllerType(moduleSource *Module, controllerType reflect.Type, methods []*MethodType) (newControllerType *ControllerType) {
+ if moduleSource == nil {
+ moduleSource = appModule
+ }
+
+ newControllerType = &ControllerType{ModuleSource: moduleSource, Type: controllerType, Methods: methods, ControllerIndexes: findControllers(controllerType)}
+ newControllerType.ControllerEvents = NewControllerTypeEvents(newControllerType)
+ newControllerType.Namespace = moduleSource.Namespace()
+ controllerName := newControllerType.Name()
+
+ // Store the first controller only in the controllers map with the unmapped namespace.
+ if _, found := controllers[controllerName]; !found {
+ controllers[controllerName] = newControllerType
+ newControllerType.ModuleSource.AddController(newControllerType)
+ if newControllerType.ModuleSource == appModule {
+ // Add the controller mapping into the global namespace
+ controllers[newControllerType.ShortName()] = newControllerType
+ }
+ } else {
+ controllerLog.Errorf("Error, attempt to register duplicate controller as %s", controllerName)
+ }
+ controllerLog.Debugf("Registered controller: %s", controllerName)
+
+ return
+}
+
+// Method searches for a given exported method (case insensitive)
+func (ct *ControllerType) Method(name string) *MethodType {
+ lowerName := strings.ToLower(name)
+ for _, method := range ct.Methods {
+ if method.lowerName == lowerName {
+ return method
+ }
+ }
+ return nil
+}
+
+// The controller name with the namespace
+func (ct *ControllerType) Name() string {
+ return ct.Namespace + ct.ShortName()
+}
+
+// The controller name without the namespace
+func (ct *ControllerType) ShortName() string {
+ return strings.ToLower(ct.Type.Name())
+}
+
+func NewControllerTypeEvents(c *ControllerType) (ce *ControllerTypeEvents) {
+ ce = &ControllerTypeEvents{}
+ // Parse the methods for the controller type, assign any control methods
+ checkType := c.Type
+ ce.check(checkType, []int{})
+ return
+}
+
+// Add in before after panic and finally, recursive call
+// Befores are ordered in revers, everything else is in order of first encountered
+func (cte *ControllerTypeEvents) check(theType reflect.Type, fieldPath []int) {
+ typeChecker := func(checkType reflect.Type) {
+ for index := 0; index < checkType.NumMethod(); index++ {
+ m := checkType.Method(index)
+ // Must be two arguments, the second returns the controller type
+ // Go cannot differentiate between promoted methods and
+ // embedded methods, this allows the embedded method to be run
+ // https://github.com/golang/go/issues/21162
+ if m.Type.NumOut() == 2 && m.Type.Out(1) == checkType {
+ if checkType.Kind() == reflect.Ptr {
+ controllerLog.Debug("Found controller type event method pointer", "name", checkType.Elem().Name(), "methodname", m.Name)
+ } else {
+ controllerLog.Debug("Found controller type event method", "name", checkType.Name(), "methodname", m.Name)
+ }
+ controllerFieldPath := newFieldPath(checkType.Kind() == reflect.Ptr, m.Func, fieldPath)
+ switch strings.ToLower(m.Name) {
+ case "before":
+ cte.Before = append([]*ControllerFieldPath{controllerFieldPath}, cte.Before...)
+ case "after":
+ cte.After = append(cte.After, controllerFieldPath)
+ case "panic":
+ cte.Panic = append(cte.Panic, controllerFieldPath)
+ case "finally":
+ cte.Finally = append(cte.Finally, controllerFieldPath)
+ }
+ }
+ }
+ }
+
+ // Check methods of both types
+ typeChecker(theType)
+ typeChecker(reflect.PtrTo(theType))
+
+ // Check for any sub controllers, ignore any pointers to controllers revel.Controller
+ for i := 0; i < theType.NumField(); i++ {
+ v := theType.Field(i)
+
+ switch v.Type.Kind() {
+ case reflect.Struct:
+ cte.check(v.Type, append(fieldPath, i))
+ }
+ }
+}
+func newFieldPath(isPointer bool, value reflect.Value, fieldPath []int) *ControllerFieldPath {
+ return &ControllerFieldPath{IsPointer: isPointer, FunctionCall: value, FieldIndexPath: fieldPath}
+}
+
+func (fieldPath *ControllerFieldPath) Invoke(value reflect.Value, input []reflect.Value) (result []reflect.Value) {
+ for _, index := range fieldPath.FieldIndexPath {
+ // You can only fetch fields from non pointers
+ if value.Type().Kind() == reflect.Ptr {
+ value = value.Elem().Field(index)
+ } else {
+ value = value.Field(index)
+ }
+ }
+ if fieldPath.IsPointer && value.Type().Kind() != reflect.Ptr {
+ value = value.Addr()
+ } else if !fieldPath.IsPointer && value.Type().Kind() == reflect.Ptr {
+ value = value.Elem()
+ }
+
+ return fieldPath.FunctionCall.Call(append([]reflect.Value{value}, input...))
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "path/filepath"
+ "runtime/debug"
+ "strconv"
+ "strings"
+)
+
+// Error description, used as an argument to the error template.
+type Error struct {
+ SourceType string // The type of source that failed to build.
+ Title, Path, Description string // Description of the error, as presented to the user.
+ Line, Column int // Where the error was encountered.
+ SourceLines []string // The entire source file, split into lines.
+ Stack string // The raw stack trace string from debug.Stack().
+ MetaError string // Error that occurred producing the error page.
+ Link string // A configurable link to wrap the error source in
+}
+
+// SourceLine structure to hold the per-source-line details.
+type SourceLine struct {
+ Source string
+ Line int
+ IsError bool
+}
+
+// NewErrorFromPanic method finds the deepest stack from in user code and
+// provide a code listing of that, on the line that eventually triggered
+// the panic. Returns nil if no relevant stack frame can be found.
+func NewErrorFromPanic(err interface{}) *Error {
+
+ // Parse the filename and line from the originating line of app code.
+ // /Users/robfig/code/gocode/src/revel/examples/booking/app/controllers/hotels.go:191 (0x44735)
+ stack := string(debug.Stack())
+ frame, basePath := findRelevantStackFrame(stack)
+ if frame == -1 {
+ return nil
+ }
+
+ stack = stack[frame:]
+ stackElement := stack[:strings.Index(stack, "\n")]
+ colonIndex := strings.LastIndex(stackElement, ":")
+ filename := stackElement[:colonIndex]
+ var line int
+ fmt.Sscan(stackElement[colonIndex+1:], &line)
+
+ // Show an error page.
+ description := "Unspecified error"
+ if err != nil {
+ description = fmt.Sprint(err)
+ }
+ lines, readErr := ReadLines(filename)
+ if readErr != nil {
+ utilLog.Error("Unable to read file", "file", filename, "error", readErr)
+ }
+ return &Error{
+ Title: "Runtime Panic",
+ Path: filename[len(basePath):],
+ Line: line,
+ Description: description,
+ SourceLines: lines,
+ Stack: stack,
+ }
+}
+
+// Error method constructs a plaintext version of the error, taking
+// account that fields are optionally set. Returns e.g. Compilation Error
+// (in views/header.html:51): expected right delim in end; got "}"
+func (e *Error) Error() string {
+ loc := ""
+ if e.Path != "" {
+ line := ""
+ if e.Line != 0 {
+ line = fmt.Sprintf(":%d", e.Line)
+ }
+ loc = fmt.Sprintf("(in %s%s)", e.Path, line)
+ }
+ header := loc
+ if e.Title != "" {
+ if loc != "" {
+ header = fmt.Sprintf("%s %s: ", e.Title, loc)
+ } else {
+ header = fmt.Sprintf("%s: ", e.Title)
+ }
+ }
+ return fmt.Sprintf("%s%s Stack: %s", header, e.Description, e.Stack)
+}
+
+// ContextSource method returns a snippet of the source around
+// where the error occurred.
+func (e *Error) ContextSource() []SourceLine {
+ if e.SourceLines == nil {
+ return nil
+ }
+ start := (e.Line - 1) - 5
+ if start < 0 {
+ start = 0
+ }
+ end := (e.Line - 1) + 5
+ if end > len(e.SourceLines) {
+ end = len(e.SourceLines)
+ }
+
+ lines := make([]SourceLine, end-start)
+ for i, src := range e.SourceLines[start:end] {
+ fileLine := start + i + 1
+ lines[i] = SourceLine{src, fileLine, fileLine == e.Line}
+ }
+ return lines
+}
+
+// SetLink method prepares a link and assign to Error.Link attribute
+func (e *Error) SetLink(errorLink string) {
+ errorLink = strings.Replace(errorLink, "{{Path}}", e.Path, -1)
+ errorLink = strings.Replace(errorLink, "{{Line}}", strconv.Itoa(e.Line), -1)
+
+ e.Link = "<a href=" + errorLink + ">" + e.Path + ":" + strconv.Itoa(e.Line) + "</a>"
+}
+
+// Return the character index of the first relevant stack frame, or -1 if none were found.
+// Additionally it returns the base path of the tree in which the identified code resides.
+func findRelevantStackFrame(stack string) (int, string) {
+ // Find first item in SourcePath that isn't in RevelPath.
+ // If first item is in RevelPath, keep track of position, trim and check again.
+ partialStack := stack
+ sourcePath := filepath.ToSlash(SourcePath)
+ revelPath := filepath.ToSlash(RevelPath)
+ sumFrame := 0
+ for {
+ frame := strings.Index(partialStack, sourcePath)
+ revelFrame := strings.Index(partialStack, revelPath)
+
+ if frame == -1 {
+ break
+ } else if frame != revelFrame {
+ return sumFrame + frame, SourcePath
+ } else {
+ // Need to at least trim off the first character so this frame isn't caught again.
+ partialStack = partialStack[frame+1:]
+ sumFrame += frame + 1
+ }
+ }
+ for _, module := range Modules {
+ if frame := strings.Index(stack, filepath.ToSlash(module.Path)); frame != -1 {
+ return frame, module.Path
+ }
+ }
+ return -1, ""
+}
--- /dev/null
+package revel
+
+type (
+ // The event type
+ Event int
+ // The event response
+ EventResponse int
+ // The handler signature
+ EventHandler func(typeOf Event, value interface{}) (responseOf EventResponse)
+)
+
+const (
+ // Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option)
+ TEMPLATE_REFRESH_REQUESTED Event = iota
+ // Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option)
+ TEMPLATE_REFRESH_COMPLETED
+ // Event type before all module loads, events thrown to handlers added to AddInitEventHandler
+
+ // Event type before all module loads, events thrown to handlers added to AddInitEventHandler
+ REVEL_BEFORE_MODULES_LOADED
+ // Event type after all module loads, events thrown to handlers added to AddInitEventHandler
+ REVEL_AFTER_MODULES_LOADED
+
+ // Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler
+ ENGINE_BEFORE_INITIALIZED
+ // Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler
+ ENGINE_STARTED
+
+ // Event raised when the engine is told to shutdown
+ ENGINE_SHUTDOWN_REQUEST
+
+ // Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler
+ ENGINE_SHUTDOWN
+
+ // Called before routes are refreshed
+ ROUTE_REFRESH_REQUESTED
+ // Called after routes have been refreshed
+ ROUTE_REFRESH_COMPLETED
+
+ // Fired when a panic is caught during the startup process
+ REVEL_FAILURE
+)
+
+// Fires system events from revel
+func RaiseEvent(key Event, value interface{}) (response EventResponse) {
+ utilLog.Info("Raising event", "len", len(initEventList))
+ for _, handler := range initEventList {
+ response |= handler(key, value)
+ }
+ return
+}
+
+// Add event handler to listen for all system events
+func AddInitEventHandler(handler EventHandler) {
+ initEventList = append(initEventList, handler)
+ return
+}
--- /dev/null
+package revel_test
+
+import (
+ "github.com/revel/revel"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+// Test that the event handler can be attached and it dispatches the event received
+func TestEventHandler(t *testing.T) {
+ counter := 0
+ newListener := func(typeOf revel.Event, value interface{}) (responseOf revel.EventResponse) {
+ if typeOf == revel.REVEL_FAILURE {
+ counter++
+ }
+ return
+ }
+ // Attach the same handlder twice so we expect to see the response twice as well
+ revel.AddInitEventHandler(newListener)
+ revel.AddInitEventHandler(newListener)
+ revel.RaiseEvent(revel.REVEL_AFTER_MODULES_LOADED, nil)
+ revel.RaiseEvent(revel.REVEL_FAILURE, nil)
+ assert.Equal(t, counter, 2, "Expected event handler to have been called")
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+)
+
+type Hotel struct {
+ HotelID int
+ Name, Address string
+ City, State, Zip string
+ Country string
+ Price int
+}
+
+type Hotels struct {
+ *Controller
+}
+
+type Static struct {
+ *Controller
+}
+
+type Implicit struct {
+ *Controller
+}
+
+type Application struct {
+ *Controller
+}
+
+func (c Hotels) Show(id int) Result {
+ title := "View Hotel"
+ hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300}
+ // The line number below must match the one with the code : RenderArgNames: map[int][]string{43: {"title", "hotel"}},
+ return c.Render(title, hotel)
+}
+
+func (c Hotels) Book(id int) Result {
+ hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300}
+ return c.RenderJSON(hotel)
+}
+
+func (c Hotels) Index() Result {
+ return c.RenderText("Hello, World!")
+}
+
+func (c Static) Serve(prefix, path string) Result {
+ var basePath, dirName string
+
+ if !filepath.IsAbs(dirName) {
+ basePath = BasePath
+ }
+
+ fname := filepath.Join(basePath, prefix, path)
+ file, err := os.Open(fname)
+ if os.IsNotExist(err) {
+ return c.NotFound("")
+ } else if err != nil {
+ RevelLog.Errorf("Problem opening file (%s): %s ", fname, err)
+ return c.NotFound("This was found but not sure why we couldn't open it.")
+ }
+ return c.RenderFile(file, "")
+}
+
+// Register controllers is in its own function so the route test can use it as well
+func registerControllers() {
+ controllers = make(map[string]*ControllerType)
+ RaiseEvent(ROUTE_REFRESH_REQUESTED, nil)
+ RegisterController((*Hotels)(nil),
+ []*MethodType{
+ {
+ Name: "Index",
+ },
+ {
+ Name: "Show",
+ Args: []*MethodArg{
+ {"id", reflect.TypeOf((*int)(nil))},
+ },
+ RenderArgNames: map[int][]string{41: {"title", "hotel"}},
+ },
+ {
+ Name: "Book",
+ Args: []*MethodArg{
+ {"id", reflect.TypeOf((*int)(nil))},
+ },
+ },
+ })
+
+ RegisterController((*Static)(nil),
+ []*MethodType{
+ {
+ Name: "Serve",
+ Args: []*MethodArg{
+ {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+ {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+ },
+ RenderArgNames: map[int][]string{},
+ },
+ })
+ RegisterController((*Implicit)(nil),
+ []*MethodType{
+ {
+ Name: "Implicit",
+ Args: []*MethodArg{
+ {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+ {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+ },
+ RenderArgNames: map[int][]string{},
+ },
+ })
+ RegisterController((*Application)(nil),
+ []*MethodType{
+ {
+ Name: "Application",
+ Args: []*MethodArg{
+ {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+ {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+ },
+ RenderArgNames: map[int][]string{},
+ },
+ {
+ Name: "Index",
+ Args: []*MethodArg{
+ {Name: "foo", Type: reflect.TypeOf((*string)(nil))},
+ {Name: "bar", Type: reflect.TypeOf((*string)(nil))},
+ },
+ RenderArgNames: map[int][]string{},
+ },
+ })
+}
+func startFakeBookingApp() {
+ Init("prod", "github.com/revel/revel/testdata", "")
+
+ MainTemplateLoader = NewTemplateLoader([]string{ViewsPath, filepath.Join(RevelPath, "templates")})
+ if err := MainTemplateLoader.Refresh(); err != nil {
+ RevelLog.Fatal("Template error","error",err)
+ }
+
+ registerControllers()
+
+ InitServerEngine(9000, GO_NATIVE_SERVER_ENGINE)
+ RaiseEvent(ENGINE_BEFORE_INITIALIZED, nil)
+ InitServer()
+
+ RaiseEvent(ENGINE_STARTED, nil)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "reflect"
+ "strings"
+)
+
+// Field represents a data field that may be collected in a web form.
+type Field struct {
+ Name string
+ Error *ValidationError
+ viewArgs map[string]interface{}
+ controller *Controller
+}
+
+func NewField(name string, viewArgs map[string]interface{}) *Field {
+ err, _ := viewArgs["errors"].(map[string]*ValidationError)[name]
+ controller, _ := viewArgs["_controller"].(*Controller)
+ return &Field{
+ Name: name,
+ Error: err,
+ viewArgs: viewArgs,
+ controller: controller,
+ }
+}
+
+// ID returns an identifier suitable for use as an HTML id.
+func (f *Field) ID() string {
+ return strings.Replace(f.Name, ".", "_", -1)
+}
+
+// Flash returns the flashed value of this Field.
+func (f *Field) Flash() string {
+ v, _ := f.viewArgs["flash"].(map[string]string)[f.Name]
+ return v
+}
+
+// Options returns the option list of this Field.
+func (f *Field) Options() []string {
+ if f.viewArgs["options"] == nil {
+ return nil
+ }
+ v, _ := f.viewArgs["options"].(map[string][]string)[f.Name]
+ return v
+}
+
+// FlashArray returns the flashed value of this Field as a list split on comma.
+func (f *Field) FlashArray() []string {
+ v := f.Flash()
+ if v == "" {
+ return []string{}
+ }
+ return strings.Split(v, ",")
+}
+
+// Value returns the current value of this Field.
+func (f *Field) Value() interface{} {
+ pieces := strings.Split(f.Name, ".")
+ answer, ok := f.viewArgs[pieces[0]]
+ if !ok {
+ return ""
+ }
+
+ val := reflect.ValueOf(answer)
+ for i := 1; i < len(pieces); i++ {
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ val = val.FieldByName(pieces[i])
+ if !val.IsValid() {
+ return ""
+ }
+ }
+
+ return val.Interface()
+}
+
+// ErrorClass returns ErrorCSSClass if this field has a validation error, else empty string.
+func (f *Field) ErrorClass() string {
+ if f.Error != nil {
+ if errorClass, ok := f.viewArgs["ERROR_CLASS"]; ok {
+ return errorClass.(string)
+ }
+ return ErrorCSSClass
+ }
+ return ""
+}
+
+// Get the short name and translate it
+func (f *Field) ShortName() string {
+ name := f.Name
+ if i := strings.LastIndex(name, "."); i > 0 {
+ name = name[i+1:]
+ }
+ return f.Translate(name)
+}
+
+// Translate the text
+func (f *Field) Translate(text string, args ...interface{}) string {
+ if f.controller != nil {
+ text = f.controller.Message(text, args...)
+ }
+ return text
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+// Filter type definition for Revel's filter
+type Filter func(c *Controller, filterChain []Filter)
+
+// Filters is the default set of global filters.
+// It may be set by the application on initialization.
+var Filters = []Filter{
+ PanicFilter, // Recover from panics and display an error page instead.
+ RouterFilter, // Use the routing table to select the right Action.
+ FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
+ ParamsFilter, // Parse parameters into Controller.Params.
+ SessionFilter, // Restore and write the session cookie.
+ FlashFilter, // Restore and write the flash cookie.
+ ValidationFilter, // Restore kept validation errors and save new ones from cookie.
+ I18nFilter, // Resolve the requested language.
+ InterceptorFilter, // Run interceptors around the action.
+ CompressFilter, // Compress the result.
+ BeforeAfterFilter,
+ ActionInvoker, // Invoke the action.
+}
+
+// NilFilter and NilChain are helpful in writing filter tests.
+var (
+ NilFilter = func(_ *Controller, _ []Filter) {}
+ NilChain = []Filter{NilFilter}
+)
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "reflect"
+ "strings"
+)
+
+// Map from "Controller" or "Controller.Method" to the Filter chain
+var filterOverrides = make(map[string][]Filter)
+
+// FilterConfigurator allows the developer configure the filter chain on a
+// per-controller or per-action basis. The filter configuration is applied by
+// the FilterConfiguringFilter, which is itself a filter stage. For example,
+//
+// Assuming:
+// Filters = []Filter{
+// RouterFilter,
+// FilterConfiguringFilter,
+// SessionFilter,
+// ActionInvoker,
+// }
+//
+// Add:
+// FilterAction(App.Action).
+// Add(OtherFilter)
+//
+// => RouterFilter, FilterConfiguringFilter, SessionFilter, OtherFilter, ActionInvoker
+//
+// Remove:
+// FilterAction(App.Action).
+// Remove(SessionFilter)
+//
+// => RouterFilter, FilterConfiguringFilter, OtherFilter, ActionInvoker
+//
+// Insert:
+// FilterAction(App.Action).
+// Insert(OtherFilter, revel.BEFORE, SessionFilter)
+//
+// => RouterFilter, FilterConfiguringFilter, OtherFilter, SessionFilter, ActionInvoker
+//
+// Filter modifications may be combined between Controller and Action. For example:
+// FilterController(App{}).
+// Add(Filter1)
+// FilterAction(App.Action).
+// Add(Filter2)
+//
+// .. would result in App.Action being filtered by both Filter1 and Filter2.
+//
+// Note: the last filter stage is not subject to the configurator. In
+// particular, Add() adds a filter to the second-to-last place.
+type FilterConfigurator struct {
+ key string // e.g. "App", "App.Action"
+ controllerName string // e.g. "App"
+}
+
+func newFilterConfigurator(controllerName, methodName string) FilterConfigurator {
+ if methodName == "" {
+ return FilterConfigurator{controllerName, controllerName}
+ }
+ return FilterConfigurator{controllerName + "." + methodName, controllerName}
+}
+
+// FilterController returns a configurator for the filters applied to all
+// actions on the given controller instance. For example:
+// FilterController(MyController{})
+func FilterController(controllerInstance interface{}) FilterConfigurator {
+ t := reflect.TypeOf(controllerInstance)
+ for t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ return newFilterConfigurator(t.Name(), "")
+}
+
+// FilterAction returns a configurator for the filters applied to the given
+// controller method. For example:
+// FilterAction(MyController.MyAction)
+func FilterAction(methodRef interface{}) FilterConfigurator {
+ var (
+ methodValue = reflect.ValueOf(methodRef)
+ methodType = methodValue.Type()
+ )
+ if methodType.Kind() != reflect.Func || methodType.NumIn() == 0 {
+ panic("Expecting a controller method reference (e.g. Controller.Action), got a " +
+ methodType.String())
+ }
+
+ controllerType := methodType.In(0)
+ method := FindMethod(controllerType, methodValue)
+ if method == nil {
+ panic("Action not found on controller " + controllerType.Name())
+ }
+
+ for controllerType.Kind() == reflect.Ptr {
+ controllerType = controllerType.Elem()
+ }
+
+ return newFilterConfigurator(controllerType.Name(), method.Name)
+}
+
+// Add the given filter in the second-to-last position in the filter chain.
+// (Second-to-last so that it is before ActionInvoker)
+func (conf FilterConfigurator) Add(f Filter) FilterConfigurator {
+ conf.apply(func(fc []Filter) []Filter {
+ return conf.addFilter(f, fc)
+ })
+ return conf
+}
+
+func (conf FilterConfigurator) addFilter(f Filter, fc []Filter) []Filter {
+ return append(fc[:len(fc)-1], f, fc[len(fc)-1])
+}
+
+// Remove a filter from the filter chain.
+func (conf FilterConfigurator) Remove(target Filter) FilterConfigurator {
+ conf.apply(func(fc []Filter) []Filter {
+ return conf.rmFilter(target, fc)
+ })
+ return conf
+}
+
+func (conf FilterConfigurator) rmFilter(target Filter, fc []Filter) []Filter {
+ for i, f := range fc {
+ if FilterEq(f, target) {
+ return append(fc[:i], fc[i+1:]...)
+ }
+ }
+ return fc
+}
+
+// Insert a filter into the filter chain before or after another.
+// This may be called with the BEFORE or AFTER constants, for example:
+// revel.FilterAction(App.Index).
+// Insert(MyFilter, revel.BEFORE, revel.ActionInvoker).
+// Insert(MyFilter2, revel.AFTER, revel.PanicFilter)
+func (conf FilterConfigurator) Insert(insert Filter, where When, target Filter) FilterConfigurator {
+ if where != BEFORE && where != AFTER {
+ panic("where must be BEFORE or AFTER")
+ }
+ conf.apply(func(fc []Filter) []Filter {
+ return conf.insertFilter(insert, where, target, fc)
+ })
+ return conf
+}
+
+func (conf FilterConfigurator) insertFilter(insert Filter, where When, target Filter, fc []Filter) []Filter {
+ for i, f := range fc {
+ if FilterEq(f, target) {
+ if where == BEFORE {
+ return append(fc[:i], append([]Filter{insert}, fc[i:]...)...)
+ }
+ return append(fc[:i+1], append([]Filter{insert}, fc[i+1:]...)...)
+ }
+ }
+ return fc
+}
+
+// getChain returns the filter chain that applies to the given controller or
+// action. If no overrides are configured, then a copy of the default filter
+// chain is returned.
+func (conf FilterConfigurator) getChain() []Filter {
+ var filters []Filter
+ if filters = getOverrideChain(conf.controllerName, conf.key); filters == nil {
+ // The override starts with all filters after FilterConfiguringFilter
+ for i, f := range Filters {
+ if FilterEq(f, FilterConfiguringFilter) {
+ filters = make([]Filter, len(Filters)-i-1)
+ copy(filters, Filters[i+1:])
+ break
+ }
+ }
+ if filters == nil {
+ panic("FilterConfiguringFilter not found in revel.Filters.")
+ }
+ }
+ return filters
+}
+
+// apply applies the given functional change to the filter overrides.
+// No other function modifies the filterOverrides map.
+func (conf FilterConfigurator) apply(f func([]Filter) []Filter) {
+ // Updates any actions that have had their filters overridden, if this is a
+ // Controller configurator.
+ if conf.controllerName == conf.key {
+ for k, v := range filterOverrides {
+ if strings.HasPrefix(k, conf.controllerName+".") {
+ filterOverrides[k] = f(v)
+ }
+ }
+ }
+
+ // Update the Controller or Action overrides.
+ filterOverrides[conf.key] = f(conf.getChain())
+}
+
+// FilterEq returns true if the two filters reference the same filter.
+func FilterEq(a, b Filter) bool {
+ return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer()
+}
+
+// FilterConfiguringFilter is a filter stage that customizes the remaining
+// filter chain for the action being invoked.
+func FilterConfiguringFilter(c *Controller, fc []Filter) {
+ if newChain := getOverrideChain(c.Name, c.Action); newChain != nil {
+ newChain[0](c, newChain[1:])
+ return
+ }
+ fc[0](c, fc[1:])
+}
+
+// getOverrideChain retrieves the overrides for the action that is set
+func getOverrideChain(controllerName, action string) []Filter {
+ if newChain, ok := filterOverrides[action]; ok {
+ return newChain
+ }
+ if newChain, ok := filterOverrides[controllerName]; ok {
+ return newChain
+ }
+ return nil
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import "testing"
+
+type FakeController struct{}
+
+func (c FakeController) Foo() {}
+func (c *FakeController) Bar() {}
+
+func TestFilterConfiguratorKey(t *testing.T) {
+ conf := FilterController(FakeController{})
+ if conf.key != "FakeController" {
+ t.Errorf("Expected key 'FakeController', was %s", conf.key)
+ }
+
+ conf = FilterController(&FakeController{})
+ if conf.key != "FakeController" {
+ t.Errorf("Expected key 'FakeController', was %s", conf.key)
+ }
+
+ conf = FilterAction(FakeController.Foo)
+ if conf.key != "FakeController.Foo" {
+ t.Errorf("Expected key 'FakeController.Foo', was %s", conf.key)
+ }
+
+ conf = FilterAction((*FakeController).Bar)
+ if conf.key != "FakeController.Bar" {
+ t.Errorf("Expected key 'FakeController.Bar', was %s", conf.key)
+ }
+}
+
+func TestFilterConfigurator(t *testing.T) {
+ // Filters is global state. Restore it after this test.
+ oldFilters := make([]Filter, len(Filters))
+ copy(oldFilters, Filters)
+ defer func() {
+ Filters = oldFilters
+ }()
+
+ Filters = []Filter{
+ RouterFilter,
+ FilterConfiguringFilter,
+ SessionFilter,
+ FlashFilter,
+ ActionInvoker,
+ }
+
+ // Do one of each operation.
+ conf := FilterAction(FakeController.Foo).
+ Add(NilFilter).
+ Remove(FlashFilter).
+ Insert(ValidationFilter, BEFORE, NilFilter).
+ Insert(I18nFilter, AFTER, NilFilter)
+ expected := []Filter{
+ SessionFilter,
+ ValidationFilter,
+ NilFilter,
+ I18nFilter,
+ ActionInvoker,
+ }
+ actual := getOverride("Foo")
+ if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+ t.Errorf("Ops failed.\nActual: %#v\nExpect: %#v\nConf:%v", actual, expected, conf)
+ }
+
+ // Action2 should be unchanged
+ if getOverride("Bar") != nil {
+ t.Errorf("Filtering Action should not affect Action2.")
+ }
+
+ // Test that combining overrides on both the Controller and Action works.
+ FilterController(FakeController{}).
+ Add(PanicFilter)
+ expected = []Filter{
+ SessionFilter,
+ ValidationFilter,
+ NilFilter,
+ I18nFilter,
+ PanicFilter,
+ ActionInvoker,
+ }
+ actual = getOverride("Foo")
+ if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+ t.Errorf("Expected PanicFilter added to Foo.\nActual: %#v\nExpect: %#v", actual, expected)
+ }
+
+ expected = []Filter{
+ SessionFilter,
+ FlashFilter,
+ PanicFilter,
+ ActionInvoker,
+ }
+ actual = getOverride("Bar")
+ if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+ t.Errorf("Expected PanicFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected)
+ }
+
+ FilterAction((*FakeController).Bar).
+ Add(NilFilter)
+ expected = []Filter{
+ SessionFilter,
+ ValidationFilter,
+ NilFilter,
+ I18nFilter,
+ PanicFilter,
+ ActionInvoker,
+ }
+ actual = getOverride("Foo")
+ if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+ t.Errorf("Expected no change to Foo.\nActual: %#v\nExpect: %#v", actual, expected)
+ }
+
+ expected = []Filter{
+ SessionFilter,
+ FlashFilter,
+ PanicFilter,
+ NilFilter,
+ ActionInvoker,
+ }
+ actual = getOverride("Bar")
+ if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+ t.Errorf("Expected NilFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected)
+ }
+}
+
+func filterSliceEqual(a, e []Filter) bool {
+ for i, f := range a {
+ if !FilterEq(f, e[i]) {
+ return false
+ }
+ }
+ return true
+}
+
+func getOverride(methodName string) []Filter {
+ return getOverrideChain("FakeController", "FakeController."+methodName)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+)
+
+// Flash represents a cookie that is overwritten on each request.
+// It allows data to be stored across one page at a time.
+// This is commonly used to implement success or error messages.
+// E.g. the Post/Redirect/Get pattern:
+// http://en.wikipedia.org/wiki/Post/Redirect/Get
+type Flash struct {
+ // `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()`
+ Data, Out map[string]string
+}
+
+// Error serializes the given msg and args to an "error" key within
+// the Flash cookie.
+func (f Flash) Error(msg string, args ...interface{}) {
+ if len(args) == 0 {
+ f.Out["error"] = msg
+ } else {
+ f.Out["error"] = fmt.Sprintf(msg, args...)
+ }
+}
+
+// Success serializes the given msg and args to a "success" key within
+// the Flash cookie.
+func (f Flash) Success(msg string, args ...interface{}) {
+ if len(args) == 0 {
+ f.Out["success"] = msg
+ } else {
+ f.Out["success"] = fmt.Sprintf(msg, args...)
+ }
+}
+
+// FlashFilter is a Revel Filter that retrieves and sets the flash cookie.
+// Within Revel, it is available as a Flash attribute on Controller instances.
+// The name of the Flash cookie is set as CookiePrefix + "_FLASH".
+func FlashFilter(c *Controller, fc []Filter) {
+ c.Flash = restoreFlash(c.Request)
+ c.ViewArgs["flash"] = c.Flash.Data
+
+ fc[0](c, fc[1:])
+
+ // Store the flash.
+ var flashValue string
+ for key, value := range c.Flash.Out {
+ flashValue += "\x00" + key + ":" + value + "\x00"
+ }
+ c.SetCookie(&http.Cookie{
+ Name: CookiePrefix + "_FLASH",
+ Value: url.QueryEscape(flashValue),
+ HttpOnly: true,
+ Secure: CookieSecure,
+ Path: "/",
+ })
+}
+
+// restoreFlash deserializes a Flash cookie struct from a request.
+func restoreFlash(req *Request) Flash {
+ flash := Flash{
+ Data: make(map[string]string),
+ Out: make(map[string]string),
+ }
+ if cookie, err := req.Cookie(CookiePrefix + "_FLASH"); err == nil {
+ ParseKeyValueCookie(cookie.GetValue(), func(key, val string) {
+ flash.Data[key] = val
+ })
+ }
+ return flash
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+
+ "context"
+ "mime/multipart"
+ "path/filepath"
+)
+
+// Request is Revel's HTTP request object structure
+type Request struct {
+ In ServerRequest // The server request
+ Header *RevelHeader // The revel header
+ ContentType string // The content type
+ Format string // The output format "html", "xml", "json", or "txt"
+ AcceptLanguages AcceptLanguages // The languages to accept
+ Locale string // THe locale
+ WebSocket ServerWebSocket // The websocket
+ Method string // The method
+ RemoteAddr string // The remote address
+ Host string // The host
+ // URL request path from the server (built)
+ URL *url.URL // The url
+ // DEPRECATED use GetForm()
+ Form url.Values // The Form
+ // DEPRECATED use GetMultipartForm()
+ MultipartForm *MultipartForm // The multipart form
+ controller *Controller // The controller, so some of this data can be fetched
+}
+
+var FORM_NOT_FOUND = errors.New("Form Not Found")
+var httpLog = RevelLog.New("section", "http")
+
+// Response is Revel's HTTP response object structure
+type Response struct {
+ Status int
+ ContentType string
+ Out OutResponse
+ writer io.Writer
+}
+
+// The output response
+type OutResponse struct {
+ // internalHeader.Server Set by ServerResponse.Get(HTTP_SERVER_HEADER), saves calling the get every time the header needs to be written to
+ internalHeader *RevelHeader // The internal header
+ Server ServerResponse // The server response
+ response *Response // The response
+}
+
+// The header defined in Revel
+type RevelHeader struct {
+ Server ServerHeader // The server
+}
+
+// NewResponse wraps ServerResponse inside a Revel's Response and returns it
+func NewResponse(w ServerResponse) (r *Response) {
+ r = &Response{Out: OutResponse{Server: w, internalHeader: &RevelHeader{}}}
+ r.Out.response = r
+ return r
+}
+
+// NewRequest returns a Revel's HTTP request instance with given HTTP instance
+func NewRequest(r ServerRequest) *Request {
+ req := &Request{Header: &RevelHeader{}}
+ if r != nil {
+ req.SetRequest(r)
+ }
+ return req
+}
+func (req *Request) SetRequest(r ServerRequest) {
+ req.In = r
+ if h, e := req.In.Get(HTTP_SERVER_HEADER); e == nil {
+ req.Header.Server = h.(ServerHeader)
+ }
+
+ req.URL, _ = req.GetValue(HTTP_URL).(*url.URL)
+ req.ContentType = ResolveContentType(req)
+ req.Format = ResolveFormat(req)
+ req.AcceptLanguages = ResolveAcceptLanguage(req)
+ req.Method, _ = req.GetValue(HTTP_METHOD).(string)
+ req.RemoteAddr, _ = req.GetValue(HTTP_REMOTE_ADDR).(string)
+ req.Host, _ = req.GetValue(HTTP_HOST).(string)
+
+}
+
+// Returns a cookie
+func (req *Request) Cookie(key string) (ServerCookie, error) {
+ if req.Header.Server != nil {
+ return req.Header.Server.GetCookie(key)
+ }
+ return nil, http.ErrNoCookie
+}
+
+// Fetch the requested URI
+func (req *Request) GetRequestURI() string {
+ uri, _ := req.GetValue(HTTP_REQUEST_URI).(string)
+ return uri
+}
+
+// Fetch the query
+func (req *Request) GetQuery() (v url.Values) {
+ v, _ = req.GetValue(ENGINE_PARAMETERS).(url.Values)
+ return
+}
+
+// Fetch the path
+func (req *Request) GetPath() (path string) {
+ path, _ = req.GetValue(ENGINE_PATH).(string)
+ return
+}
+
+// Fetch the body
+func (req *Request) GetBody() (body io.Reader) {
+ body, _ = req.GetValue(HTTP_BODY).(io.Reader)
+ return
+}
+
+// Fetch the context
+func (req *Request) Context() (c context.Context) {
+ c, _ = req.GetValue(HTTP_REQUEST_CONTEXT).(context.Context)
+ return
+}
+
+// Deprecated use controller.Params.Get()
+func (req *Request) FormValue(key string) (value string) {
+ return req.controller.Params.Get(key)
+}
+
+// Deprecated use controller.Params.Form[Key]
+func (req *Request) PostFormValue(key string) (value string) {
+ valueList := req.controller.Params.Form[key]
+ if len(valueList) > 0 {
+ value = valueList[0]
+ }
+ return
+}
+
+// Deprecated use GetForm() instead
+func (req *Request) ParseForm() (e error) {
+ if req.Form == nil {
+ req.Form, e = req.GetForm()
+ }
+ return
+}
+
+func (req *Request) GetForm() (url.Values, error) {
+ if form, err := req.In.Get(HTTP_FORM); err != nil {
+ return nil, err
+ } else if values, found := form.(url.Values); found {
+ req.Form = values
+ return values, nil
+ }
+ return nil, FORM_NOT_FOUND
+}
+
+// Deprecated for backwards compatibility only
+type MultipartForm struct {
+ File map[string][]*multipart.FileHeader
+ Value url.Values
+ origin ServerMultipartForm
+}
+
+func (req *Request) MultipartReader() (*multipart.Reader, error) {
+
+ return nil, errors.New("MultipartReader not supported, use controller.Param")
+}
+
+// Deprecated for backwards compatibility only
+func newMultipareForm(s ServerMultipartForm) (f *MultipartForm) {
+ return &MultipartForm{File: s.GetFiles(), Value: s.GetValues(), origin: s}
+}
+
+// Deprecated use GetMultipartForm() instead
+func (req *Request) ParseMultipartForm(_ int64) (e error) {
+ var s ServerMultipartForm
+ if s, e = req.GetMultipartForm(); e == nil {
+ req.MultipartForm = newMultipareForm(s)
+ }
+ return
+}
+
+// Return the args for the controller
+func (req *Request) Args() map[string]interface{} {
+ return req.controller.Args
+}
+
+// Return a multipart form
+func (req *Request) GetMultipartForm() (ServerMultipartForm, error) {
+ if form, err := req.In.Get(HTTP_MULTIPART_FORM); err != nil {
+ return nil, err
+ } else if values, found := form.(ServerMultipartForm); found {
+ return values, nil
+ }
+ return nil, FORM_NOT_FOUND
+}
+
+// Destroy the request
+func (req *Request) Destroy() {
+ req.In = nil
+ req.ContentType = ""
+ req.Format = ""
+ req.AcceptLanguages = nil
+ req.Method = ""
+ req.RemoteAddr = ""
+ req.Host = ""
+ req.Header.Destroy()
+ req.URL = nil
+ req.Form = nil
+ req.MultipartForm = nil
+}
+
+// Set the server response
+func (resp *Response) SetResponse(r ServerResponse) {
+ resp.Out.Server = r
+ if h, e := r.Get(HTTP_SERVER_HEADER); e == nil {
+ resp.Out.internalHeader.Server, _ = h.(ServerHeader)
+ }
+}
+
+// Destroy the output response
+func (o *OutResponse) Destroy() {
+ o.response = nil
+ o.internalHeader.Destroy()
+}
+
+// Destroy the RevelHeader
+func (h *RevelHeader) Destroy() {
+ h.Server = nil
+}
+
+// Destroy the Response
+func (resp *Response) Destroy() {
+ resp.Out.Destroy()
+ resp.Status = 0
+ resp.ContentType = ""
+ resp.writer = nil
+}
+
+// UserAgent returns the client's User-Agent header string.
+func (r *Request) UserAgent() string {
+ return r.Header.Get("User-Agent")
+}
+
+// Referer returns the client's Referer header string.
+func (req *Request) Referer() string {
+ return req.Header.Get("Referer")
+}
+
+// Return the httpheader for the key
+func (req *Request) GetHttpHeader(key string) string {
+ return req.Header.Get(key)
+}
+
+// Return the value from the server
+func (r *Request) GetValue(key int) (value interface{}) {
+ value, _ = r.In.Get(key)
+ return
+}
+
+// WriteHeader writes the header (for now, just the status code).
+// The status may be set directly by the application (c.Response.Status = 501).
+// If it isn't, then fall back to the provided status code.
+func (resp *Response) WriteHeader(defaultStatusCode int, defaultContentType string) {
+ if resp.ContentType == "" {
+ resp.ContentType = defaultContentType
+ }
+ resp.Out.internalHeader.Set("Content-Type", resp.ContentType)
+ if resp.Status == 0 {
+ resp.Status = defaultStatusCode
+ }
+ resp.SetStatus(resp.Status)
+}
+func (resp *Response) SetStatus(statusCode int) {
+ if resp.Out.internalHeader.Server != nil {
+ resp.Out.internalHeader.Server.SetStatus(statusCode)
+ } else {
+ resp.Out.Server.Set(ENGINE_RESPONSE_STATUS, statusCode)
+ }
+
+}
+
+// Return the writer
+func (resp *Response) GetWriter() (writer io.Writer) {
+ writer = resp.writer
+ if writer == nil {
+ if w, e := resp.Out.Server.Get(ENGINE_WRITER); e == nil {
+ writer, resp.writer = w.(io.Writer), w.(io.Writer)
+ }
+ }
+
+ return
+}
+
+// Replace the writer
+func (resp *Response) SetWriter(writer io.Writer) bool {
+ resp.writer = writer
+ // Leave it up to the engine to flush and close the writer
+ return resp.Out.Server.Set(ENGINE_WRITER, writer)
+}
+
+// Passes full control to the response to the caller - terminates any initial writes
+func (resp *Response) GetStreamWriter() (writer StreamWriter) {
+ if w, e := resp.Out.Server.Get(HTTP_STREAM_WRITER); e == nil {
+ writer = w.(StreamWriter)
+ }
+ return
+}
+
+// Return the header
+func (o *OutResponse) Header() *RevelHeader {
+ return o.internalHeader
+}
+
+// Write the header out
+func (o *OutResponse) Write(data []byte) (int, error) {
+ return o.response.GetWriter().Write(data)
+}
+
+// Set a value in the header
+func (h *RevelHeader) Set(key, value string) {
+ if h.Server != nil {
+ h.Server.Set(key, value)
+ }
+}
+
+// Add a key to the header
+func (h *RevelHeader) Add(key, value string) {
+ if h.Server != nil {
+ h.Server.Add(key, value)
+ }
+}
+
+// Set a cookie in the header
+func (h *RevelHeader) SetCookie(cookie string) {
+ if h.Server != nil {
+ h.Server.SetCookie(cookie)
+ }
+}
+
+// Set the status for the header
+func (h *RevelHeader) SetStatus(status int) {
+ if h.Server != nil {
+ h.Server.SetStatus(status)
+ }
+}
+
+// Get a key from the header
+func (h *RevelHeader) Get(key string) (value string) {
+ values := h.GetAll(key)
+ if len(values) > 0 {
+ value = values[0]
+ }
+ return
+}
+
+// GetAll returns []string of items (the header split by a comma)
+func (h *RevelHeader) GetAll(key string) (values []string) {
+ if h.Server != nil {
+ values = h.Server.Get(key)
+ }
+ return
+}
+
+// ResolveContentType gets the content type.
+// e.g. From "multipart/form-data; boundary=--" to "multipart/form-data"
+// If none is specified, returns "text/html" by default.
+func ResolveContentType(req *Request) string {
+
+ contentType := req.Header.Get("Content-Type")
+ if contentType == "" {
+ return "text/html"
+ }
+ return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
+}
+
+// ResolveFormat maps the request's Accept MIME type declaration to
+// a Request.Format attribute, specifically "html", "xml", "json", or "txt",
+// returning a default of "html" when Accept header cannot be mapped to a
+// value above.
+func ResolveFormat(req *Request) string {
+ ext := strings.ToLower(filepath.Ext(req.GetPath()))
+ switch ext {
+ case ".html":
+ return "html"
+ case ".json":
+ return "json"
+ case ".xml":
+ return "xml"
+ case ".txt":
+ return "txt"
+ }
+
+ accept := req.GetHttpHeader("accept")
+
+ switch {
+ case accept == "",
+ strings.HasPrefix(accept, "*/*"), // */
+ strings.Contains(accept, "application/xhtml"),
+ strings.Contains(accept, "text/html"):
+ return "html"
+ case strings.Contains(accept, "application/json"),
+ strings.Contains(accept, "text/javascript"),
+ strings.Contains(accept, "application/javascript"):
+ return "json"
+ case strings.Contains(accept, "application/xml"),
+ strings.Contains(accept, "text/xml"):
+ return "xml"
+ case strings.Contains(accept, "text/plain"):
+ return "txt"
+ }
+
+ return "html"
+}
+
+// AcceptLanguage is a single language from the Accept-Language HTTP header.
+type AcceptLanguage struct {
+ Language string
+ Quality float32
+}
+
+// AcceptLanguages is collection of sortable AcceptLanguage instances.
+type AcceptLanguages []AcceptLanguage
+
+func (al AcceptLanguages) Len() int { return len(al) }
+func (al AcceptLanguages) Swap(i, j int) { al[i], al[j] = al[j], al[i] }
+func (al AcceptLanguages) Less(i, j int) bool { return al[i].Quality > al[j].Quality }
+func (al AcceptLanguages) String() string {
+ output := bytes.NewBufferString("")
+ for i, language := range al {
+ if _, err := output.WriteString(fmt.Sprintf("%s (%1.1f)", language.Language, language.Quality)); err != nil {
+ httpLog.Error("String: WriteString failed:", "error", err)
+ }
+ if i != len(al)-1 {
+ if _, err := output.WriteString(", "); err != nil {
+ httpLog.Error("String: WriteString failed:", "error", err)
+ }
+ }
+ }
+ return output.String()
+}
+
+// ResolveAcceptLanguage returns a sorted list of Accept-Language
+// header values.
+//
+// The results are sorted using the quality defined in the header for each
+// language range with the most qualified language range as the first
+// element in the slice.
+//
+// See the HTTP header fields specification
+// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) for more details.
+func ResolveAcceptLanguage(req *Request) AcceptLanguages {
+ header := req.Header.Get("Accept-Language")
+ if header == "" {
+ return req.AcceptLanguages
+ }
+
+ acceptLanguageHeaderValues := strings.Split(header, ",")
+ acceptLanguages := make(AcceptLanguages, len(acceptLanguageHeaderValues))
+
+ for i, languageRange := range acceptLanguageHeaderValues {
+ if qualifiedRange := strings.Split(languageRange, ";q="); len(qualifiedRange) == 2 {
+ quality, err := strconv.ParseFloat(qualifiedRange[1], 32)
+ if err != nil {
+ httpLog.Warn("Detected malformed Accept-Language header quality in assuming quality is 1", "languageRange", languageRange)
+ acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], 1}
+ } else {
+ acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], float32(quality)}
+ }
+ } else {
+ acceptLanguages[i] = AcceptLanguage{languageRange, 1}
+ }
+ }
+
+ sort.Sort(acceptLanguages)
+ return acceptLanguages
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/revel/config"
+)
+
+const (
+ // CurrentLocaleViewArg the key for the current locale view arg value
+ CurrentLocaleViewArg = "currentLocale"
+
+ messageFilesDirectory = "messages"
+ messageFilePattern = `^\w+\.[a-zA-Z]{2}$`
+ defaultUnknownFormat = "??? %s ???"
+ unknownFormatConfigKey = "i18n.unknown_format"
+ defaultLanguageOption = "i18n.default_language"
+ localeCookieConfigKey = "i18n.cookie"
+)
+
+var (
+ // All currently loaded message configs.
+ messages map[string]*config.Config
+ localeParameterName string
+ i18nLog = RevelLog.New("section", "i18n")
+)
+
+// MessageFunc allows you to override the translation interface.
+//
+// Set this to your own function that translates to the current locale.
+// This allows you to set up your own loading and logging of translated texts.
+//
+// See Message(...) in i18n.go for example of function.
+var MessageFunc = Message
+
+// MessageLanguages returns all currently loaded message languages.
+func MessageLanguages() []string {
+ languages := make([]string, len(messages))
+ i := 0
+ for language := range messages {
+ languages[i] = language
+ i++
+ }
+ return languages
+}
+
+// Message performs a message look-up for the given locale and message using the given arguments.
+//
+// When either an unknown locale or message is detected, a specially formatted string is returned.
+func Message(locale, message string, args ...interface{}) string {
+ language, region := parseLocale(locale)
+ unknownValueFormat := getUnknownValueFormat()
+
+ messageConfig, knownLanguage := messages[language]
+ if !knownLanguage {
+ i18nLog.Debugf("Unsupported language for locale '%s' and message '%s', trying default language", locale, message)
+
+ if defaultLanguage, found := Config.String(defaultLanguageOption); found {
+ i18nLog.Debugf("Using default language '%s'", defaultLanguage)
+
+ messageConfig, knownLanguage = messages[defaultLanguage]
+ if !knownLanguage {
+ i18nLog.Debugf("Unsupported default language for locale '%s' and message '%s'", defaultLanguage, message)
+ return fmt.Sprintf(unknownValueFormat, message)
+ }
+ } else {
+ i18nLog.Warnf("Unable to find default language option (%s); messages for unsupported locales will never be translated", defaultLanguageOption)
+ return fmt.Sprintf(unknownValueFormat, message)
+ }
+ }
+
+ // This works because unlike the goconfig documentation suggests it will actually
+ // try to resolve message in DEFAULT if it did not find it in the given section.
+ value, err := messageConfig.String(region, message)
+ if err != nil {
+ i18nLog.Warnf("Unknown message '%s' for locale '%s'", message, locale)
+ return fmt.Sprintf(unknownValueFormat, message)
+ }
+
+ if len(args) > 0 {
+ i18nLog.Debugf("Arguments detected, formatting '%s' with %v", value, args)
+ safeArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ switch a := arg.(type) {
+ case template.HTML:
+ safeArgs = append(safeArgs, a)
+ case string:
+ safeArgs = append(safeArgs, template.HTML(template.HTMLEscapeString(a)))
+ default:
+ safeArgs = append(safeArgs, a)
+ }
+ }
+ value = fmt.Sprintf(value, safeArgs...)
+ }
+
+ return value
+}
+
+func parseLocale(locale string) (language, region string) {
+ if strings.Contains(locale, "-") {
+ languageAndRegion := strings.Split(locale, "-")
+ return languageAndRegion[0], languageAndRegion[1]
+ }
+
+ return locale, ""
+}
+
+// Retrieve message format or default format when i18n message is missing.
+func getUnknownValueFormat() string {
+ return Config.StringDefault(unknownFormatConfigKey, defaultUnknownFormat)
+}
+
+// Recursively read and cache all available messages from all message files on the given path.
+func loadMessages(path string) {
+ messages = make(map[string]*config.Config)
+
+ // Read in messages from the modules. Load the module messges first,
+ // so that it can be override in parent application
+ for _, module := range Modules {
+ i18nLog.Debug("Importing messages from module:", "importpath", module.ImportPath)
+ if err := Walk(filepath.Join(module.Path, messageFilesDirectory), loadMessageFile); err != nil &&
+ !os.IsNotExist(err) {
+ i18nLog.Error("Error reading messages files from module:", "error", err)
+ }
+ }
+
+ if err := Walk(path, loadMessageFile); err != nil && !os.IsNotExist(err) {
+ i18nLog.Error("Error reading messages files:", "error", err)
+ }
+}
+
+// Load a single message file
+func loadMessageFile(path string, info os.FileInfo, osError error) error {
+ if osError != nil {
+ return osError
+ }
+ if info.IsDir() {
+ return nil
+ }
+
+ if matched, _ := regexp.MatchString(messageFilePattern, info.Name()); matched {
+ messageConfig, err := parseMessagesFile(path)
+ if err != nil {
+ return err
+ }
+ locale := parseLocaleFromFileName(info.Name())
+
+ // If we have already parsed a message file for this locale, merge both
+ if _, exists := messages[locale]; exists {
+ messages[locale].Merge(messageConfig)
+ i18nLog.Debugf("Successfully merged messages for locale '%s'", locale)
+ } else {
+ messages[locale] = messageConfig
+ }
+
+ i18nLog.Debug("Successfully loaded messages from file", "file", info.Name())
+ } else {
+ i18nLog.Warn("Ignoring file because it did not have a valid extension", "file", info.Name())
+ }
+
+ return nil
+}
+
+func parseMessagesFile(path string) (messageConfig *config.Config, err error) {
+ messageConfig, err = config.ReadDefault(path)
+ return
+}
+
+func parseLocaleFromFileName(file string) string {
+ extension := filepath.Ext(file)[1:]
+ return strings.ToLower(extension)
+}
+
+func init() {
+ OnAppStart(func() {
+ loadMessages(filepath.Join(BasePath, messageFilesDirectory))
+ localeParameterName = Config.StringDefault("i18n.locale.parameter", "")
+ }, 0)
+}
+
+func I18nFilter(c *Controller, fc []Filter) {
+ foundLocale := false
+ // Search for a parameter first
+ if localeParameterName != "" {
+ if locale, found := c.Params.Values[localeParameterName]; found && len(locale[0]) > 0 {
+ setCurrentLocaleControllerArguments(c, locale[0])
+ foundLocale = true
+ i18nLog.Debug("Found locale parameter value: ", "locale", locale[0])
+ }
+ }
+ if !foundLocale {
+ if foundCookie, cookieValue := hasLocaleCookie(c.Request); foundCookie {
+ i18nLog.Debug("Found locale cookie value: ", "cookie", cookieValue)
+ setCurrentLocaleControllerArguments(c, cookieValue)
+ } else if foundHeader, headerValue := hasAcceptLanguageHeader(c.Request); foundHeader {
+ i18nLog.Debug("Found Accept-Language header value: ", "header", headerValue)
+ setCurrentLocaleControllerArguments(c, headerValue)
+ } else {
+ i18nLog.Debug("Unable to find locale in cookie or header, using empty string")
+ setCurrentLocaleControllerArguments(c, "")
+ }
+ }
+ fc[0](c, fc[1:])
+}
+
+// Set the current locale controller argument (CurrentLocaleControllerArg) with the given locale.
+func setCurrentLocaleControllerArguments(c *Controller, locale string) {
+ c.Request.Locale = locale
+ c.ViewArgs[CurrentLocaleViewArg] = locale
+}
+
+// Determine whether the given request has valid Accept-Language value.
+//
+// Assumes that the accept languages stored in the request are sorted according to quality, with top
+// quality first in the slice.
+func hasAcceptLanguageHeader(request *Request) (bool, string) {
+ if request.AcceptLanguages != nil && len(request.AcceptLanguages) > 0 {
+ return true, request.AcceptLanguages[0].Language
+ }
+
+ return false, ""
+}
+
+// Determine whether the given request has a valid language cookie value.
+func hasLocaleCookie(request *Request) (bool, string) {
+ if request != nil {
+ name := Config.StringDefault(localeCookieConfigKey, CookiePrefix+"_LANG")
+ cookie, err := request.Cookie(name)
+ if err == nil {
+ return true, cookie.GetValue()
+ }
+ i18nLog.Debug("Unable to read locale cookie ", "name", name, "error", err)
+ }
+
+ return false, ""
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "html/template"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/revel/config"
+ "github.com/revel/revel/logger"
+)
+
+const (
+ testDataPath string = "testdata/i18n"
+ testConfigPath string = "testdata/i18n/config"
+ testConfigName string = "test_app.conf"
+)
+
+func TestI18nLoadMessages(t *testing.T) {
+ loadMessages(testDataPath)
+
+ // Assert that we have the expected number of languages
+ if len(MessageLanguages()) != 2 {
+ t.Fatalf("Expected messages to contain no more or less than 2 languages, instead there are %d languages", len(MessageLanguages()))
+ }
+}
+
+func TestI18nMessage(t *testing.T) {
+ loadMessages(testDataPath)
+ loadTestI18nConfig(t)
+
+ // Assert that we can get a message and we get the expected return value
+ if message := Message("nl", "greeting"); message != "Hallo" {
+ t.Errorf("Message 'greeting' for locale 'nl' (%s) does not have the expected value", message)
+ }
+ if message := Message("en", "greeting"); message != "Hello" {
+ t.Errorf("Message 'greeting' for locale 'en' (%s) does not have the expected value", message)
+ }
+ if message := Message("en", "folded"); message != "Greeting is 'Hello'" {
+ t.Error("Unexpected unfolded message: ", message)
+ }
+ if message := Message("en", "arguments.string", "Vincent Hanna"); message != "My name is Vincent Hanna" {
+ t.Errorf("Message 'arguments.string' for locale 'en' (%s) does not have the expected value", message)
+ }
+ if message := Message("en", "arguments.hex", 1234567, 1234567); message != "The number 1234567 in hexadecimal notation would be 12d687" {
+ t.Errorf("Message 'arguments.hex' for locale 'en' (%s) does not have the expected value", message)
+ }
+ if message := Message("en", "folded.arguments", 12345); message != "Rob is 12345 years old" {
+ t.Errorf("Message 'folded.arguments' for locale 'en' (%s) does not have the expected value", message)
+ }
+ if message := Message("en-AU", "greeting"); message != "G'day" {
+ t.Errorf("Message 'greeting' for locale 'en-AU' (%s) does not have the expected value", message)
+ }
+ if message := Message("en-AU", "only_exists_in_default"); message != "Default" {
+ t.Errorf("Message 'only_exists_in_default' for locale 'en-AU' (%s) does not have the expected value", message)
+ }
+
+ // Assert that message merging works
+ if message := Message("en", "greeting2"); message != "Yo!" {
+ t.Errorf("Message 'greeting2' for locale 'en' (%s) does not have the expected value", message)
+ }
+
+ // Assert that we get the expected return value for a locale that doesn't exist
+ if message := Message("unknown locale", "message"); message != "??? message ???" {
+ t.Error("Locale 'unknown locale' is not supposed to exist")
+ }
+ // Assert that we get the expected return value for a message that doesn't exist
+ if message := Message("nl", "unknown message"); message != "??? unknown message ???" {
+ t.Error("Message 'unknown message' is not supposed to exist")
+ }
+ // XSS
+ if message := Message("en", "arguments.string", "<img src=a onerror=alert(1) />"); message != "My name is <img src=a onerror=alert(1) />" {
+ t.Error("XSS protection for messages is broken:", message)
+ }
+ // Avoid escaping HTML
+ if message := Message("en", "arguments.string", template.HTML("<img src=a onerror=alert(1) />")); message != "My name is <img src=a onerror=alert(1) />" {
+ t.Error("Passing safe HTML to message is broken:", message)
+ }
+}
+
+func TestI18nMessageWithDefaultLocale(t *testing.T) {
+ loadMessages(testDataPath)
+ loadTestI18nConfig(t)
+
+ if message := Message("doesn't exist", "greeting"); message != "Hello" {
+ t.Errorf("Expected message '%s' for unknown locale to be default '%s' but was '%s'", "greeting", "Hello", message)
+ }
+ if message := Message("doesn't exist", "unknown message"); message != "??? unknown message ???" {
+ t.Error("Message 'unknown message' is not supposed to exist in the default language")
+ }
+}
+
+func TestHasLocaleCookie(t *testing.T) {
+ loadTestI18nConfig(t)
+
+ if found, value := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en").Request); !found {
+ t.Errorf("Expected %s cookie with value '%s' but found nothing or unexpected value '%s'", "APP_LANG", "en", value)
+ }
+ if found, value := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en-US").Request); !found {
+ t.Errorf("Expected %s cookie with value '%s' but found nothing or unexpected value '%s'", "APP_LANG", "en-US", value)
+ }
+ if found, _ := hasLocaleCookie(buildRequestWithCookie("DOESNT_EXIST", "en-US").Request); found {
+ t.Errorf("Expected %s cookie to not exist, but apparently it does", "DOESNT_EXIST")
+ }
+}
+
+func TestHasLocaleCookieWithInvalidConfig(t *testing.T) {
+ loadTestI18nConfigWithoutLanguageCookieOption(t)
+ if found, _ := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en-US").Request); found {
+ t.Errorf("Expected %s cookie to not exist because the configured name is missing", "APP_LANG")
+ }
+ if found, _ := hasLocaleCookie(buildRequestWithCookie("REVEL_LANG", "en-US").Request); !found {
+ t.Errorf("Expected %s cookie to exist", "REVEL_LANG")
+ }
+}
+
+func TestHasAcceptLanguageHeader(t *testing.T) {
+ if found, value := hasAcceptLanguageHeader(buildRequestWithAcceptLanguages("en-US").Request); !found && value != "en-US" {
+ t.Errorf("Expected to find Accept-Language header with value '%s', found '%s' instead", "en-US", value)
+ }
+ if found, value := hasAcceptLanguageHeader(buildRequestWithAcceptLanguages("en-GB", "en-US", "nl").Request); !found && value != "en-GB" {
+ t.Errorf("Expected to find Accept-Language header with value '%s', found '%s' instead", "en-GB", value)
+ }
+}
+
+func TestBeforeRequest(t *testing.T) {
+ loadTestI18nConfig(t)
+
+ c := buildEmptyRequest()
+ if I18nFilter(c, NilChain); c.Request.Locale != "" {
+ t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "", c.Request.Locale)
+ }
+
+ c = buildRequestWithCookie("APP_LANG", "en-US")
+ if I18nFilter(c, NilChain); c.Request.Locale != "en-US" {
+ t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "en-US", c.Request.Locale)
+ }
+
+ c = buildRequestWithAcceptLanguages("en-GB", "en-US")
+ if I18nFilter(c, NilChain); c.Request.Locale != "en-GB" {
+ t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "en-GB", c.Request.Locale)
+ }
+}
+
+func TestI18nMessageUnknownValueFormat(t *testing.T) {
+ loadMessages(testDataPath)
+ loadTestI18nConfigWithUnknowFormatOption(t)
+
+ // Assert that we can get a message and we get the expected return value
+ if message := Message("en", "greeting"); message != "Hello" {
+ t.Errorf("Message 'greeting' for locale 'en' (%s) does not have the expected value", message)
+ }
+
+ // Assert that we get the expected return value with original format
+ if message := Message("unknown locale", "message"); message != "*** message ***" {
+ t.Error("Locale 'unknown locale' is not supposed to exist")
+ }
+ if message := Message("nl", "unknown message"); message != "*** unknown message ***" {
+ t.Error("Message 'unknown message' is not supposed to exist")
+ }
+}
+
+func BenchmarkI18nLoadMessages(b *testing.B) {
+ excludeFromTimer(b, func() {
+ RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+ return nil
+ }))
+ })
+
+ for i := 0; i < b.N; i++ {
+ loadMessages(testDataPath)
+ }
+}
+
+func BenchmarkI18nMessage(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Message("nl", "greeting")
+ }
+}
+
+func BenchmarkI18nMessageWithArguments(b *testing.B) {
+ excludeFromTimer(b, func() {
+ RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+ return nil
+ }))
+ })
+
+
+ for i := 0; i < b.N; i++ {
+ Message("en", "arguments.string", "Vincent Hanna")
+ }
+}
+
+func BenchmarkI18nMessageWithFoldingAndArguments(b *testing.B) {
+ excludeFromTimer(b, func() {
+ RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+ return nil
+ }))
+ })
+
+
+ for i := 0; i < b.N; i++ {
+ Message("en", "folded.arguments", 12345)
+ }
+}
+
+// Exclude whatever operations the given function performs from the benchmark timer.
+func excludeFromTimer(b *testing.B, f func()) {
+ b.StopTimer()
+ f()
+ b.StartTimer()
+}
+
+func loadTestI18nConfig(t *testing.T) {
+ ConfPaths = append(ConfPaths, testConfigPath)
+ testConfig, err := config.LoadContext(testConfigName, ConfPaths)
+ if err != nil {
+ t.Fatalf("Unable to load test config '%s': %s", testConfigName, err.Error())
+ }
+ Config = testConfig
+ CookiePrefix = Config.StringDefault("cookie.prefix", "REVEL")
+}
+
+func loadTestI18nConfigWithoutLanguageCookieOption(t *testing.T) {
+ loadTestI18nConfig(t)
+ Config.Raw().RemoveOption("DEFAULT", "i18n.cookie")
+}
+
+func loadTestI18nConfigWithUnknowFormatOption(t *testing.T) {
+ loadTestI18nConfig(t)
+ Config.Raw().AddOption("DEFAULT", "i18n.unknown_format", "*** %s ***")
+}
+
+func buildRequestWithCookie(name, value string) *Controller {
+ httpRequest, _ := http.NewRequest("GET", "/", nil)
+ controller := NewTestController(nil, httpRequest)
+
+ httpRequest.AddCookie(&http.Cookie{
+ Name: name,
+ Value: value,
+ Path: "",
+ Domain: "",
+ Expires: time.Now(),
+ RawExpires: "",
+ MaxAge: 0,
+ Secure: false,
+ HttpOnly: false,
+ Raw: "",
+ Unparsed: nil,
+ })
+ return controller
+}
+
+func buildRequestWithAcceptLanguages(acceptLanguages ...string) *Controller {
+ httpRequest, _ := http.NewRequest("GET", "/", nil)
+ controller := NewTestController(nil, httpRequest)
+
+ request := controller.Request
+ for _, acceptLanguage := range acceptLanguages {
+ request.AcceptLanguages = append(request.AcceptLanguages, AcceptLanguage{acceptLanguage, 1})
+ }
+ return controller
+}
+
+func buildEmptyRequest() *Controller {
+ httpRequest, _ := http.NewRequest("GET", "/", nil)
+ controller := NewTestController(nil, httpRequest)
+ return controller
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "log"
+ "reflect"
+ "sort"
+)
+
+// An InterceptorFunc is functionality invoked by the framework BEFORE or AFTER
+// an action.
+//
+// An interceptor may optionally return a Result (instead of nil). Depending on
+// when the interceptor was invoked, the response is different:
+// 1. BEFORE: No further interceptors are invoked, and neither is the action.
+// 2. AFTER: Further interceptors are still run.
+// In all cases, any returned Result will take the place of any existing Result.
+//
+// In the BEFORE case, that returned Result is guaranteed to be final, while
+// in the AFTER case it is possible that a further interceptor could emit its
+// own Result.
+//
+// Interceptors are called in the order that they are added.
+//
+// ***
+//
+// Two types of interceptors are provided: Funcs and Methods
+//
+// Func Interceptors may apply to any / all Controllers.
+//
+// func example(*revel.Controller) revel.Result
+//
+// Method Interceptors are provided so that properties can be set on application
+// controllers.
+//
+// func (c AppController) example() revel.Result
+// func (c *AppController) example() revel.Result
+//
+type InterceptorFunc func(*Controller) Result
+type InterceptorMethod interface{}
+type When int
+
+const (
+ BEFORE When = iota
+ AFTER
+ PANIC
+ FINALLY
+)
+
+type InterceptTarget int
+
+const (
+ AllControllers InterceptTarget = iota
+)
+
+type Interception struct {
+ When When
+
+ function InterceptorFunc
+ method InterceptorMethod
+
+ callable reflect.Value
+ target reflect.Type
+ interceptAll bool
+}
+
+// Invoke performs the given interception.
+// val is a pointer to the App Controller.
+func (i Interception) Invoke(val reflect.Value, target *reflect.Value) reflect.Value {
+ var arg reflect.Value
+ if i.function == nil {
+ // If it's an InterceptorMethod, then we have to pass in the target type.
+ arg = *target
+ } else {
+ // If it's an InterceptorFunc, then the type must be *Controller.
+ // We can find that by following the embedded types up the chain.
+ for val.Type() != controllerPtrType {
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ val = val.Field(0)
+ }
+ arg = val
+ }
+
+ vals := i.callable.Call([]reflect.Value{arg})
+ return vals[0]
+}
+
+func InterceptorFilter(c *Controller, fc []Filter) {
+ defer invokeInterceptors(FINALLY, c)
+ defer func() {
+ if err := recover(); err != nil {
+ invokeInterceptors(PANIC, c)
+ panic(err)
+ }
+ }()
+
+ // Invoke the BEFORE interceptors and return early, if we get a result.
+ invokeInterceptors(BEFORE, c)
+ if c.Result != nil {
+ return
+ }
+
+ fc[0](c, fc[1:])
+ invokeInterceptors(AFTER, c)
+}
+
+func invokeInterceptors(when When, c *Controller) {
+ var (
+ app = reflect.ValueOf(c.AppController)
+ result Result
+ )
+
+ for _, intc := range getInterceptors(when, app) {
+ resultValue := intc.Interceptor.Invoke(app, &intc.Target)
+ if !resultValue.IsNil() {
+ result = resultValue.Interface().(Result)
+ }
+ if when == BEFORE && result != nil {
+ c.Result = result
+ return
+ }
+ }
+ if result != nil {
+ c.Result = result
+ }
+}
+
+var interceptors []*Interception
+
+// InterceptFunc installs a general interceptor.
+// This can be applied to any Controller.
+// It must have the signature of:
+// func example(c *revel.Controller) revel.Result
+func InterceptFunc(intc InterceptorFunc, when When, target interface{}) {
+ interceptors = append(interceptors, &Interception{
+ When: when,
+ function: intc,
+ callable: reflect.ValueOf(intc),
+ target: reflect.TypeOf(target),
+ interceptAll: target == AllControllers,
+ })
+}
+
+// InterceptMethod installs an interceptor method that applies to its own Controller.
+// func (c AppController) example() revel.Result
+// func (c *AppController) example() revel.Result
+func InterceptMethod(intc InterceptorMethod, when When) {
+ methodType := reflect.TypeOf(intc)
+ if methodType.Kind() != reflect.Func || methodType.NumOut() != 1 || methodType.NumIn() != 1 {
+ log.Fatalln("Interceptor method should have signature like",
+ "'func (c *AppController) example() revel.Result' but was", methodType)
+ }
+
+ interceptors = append(interceptors, &Interception{
+ When: when,
+ method: intc,
+ callable: reflect.ValueOf(intc),
+ target: methodType.In(0),
+ })
+}
+
+// This item is used to provide a sortable set to be returned to the caller. This ensures calls order is maintained
+//
+type interceptorItem struct {
+ Interceptor *Interception
+ Target reflect.Value
+ Level int
+}
+type interceptorItemList []*interceptorItem
+
+func (a interceptorItemList) Len() int { return len(a) }
+func (a interceptorItemList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a interceptorItemList) Less(i, j int) bool { return a[i].Level < a[j].Level }
+
+type reverseInterceptorItemList []*interceptorItem
+
+func (a reverseInterceptorItemList) Len() int { return len(a) }
+func (a reverseInterceptorItemList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a reverseInterceptorItemList) Less(i, j int) bool { return a[i].Level > a[j].Level }
+func getInterceptors(when When, val reflect.Value) interceptorItemList {
+ result := interceptorItemList{}
+ for _, intc := range interceptors {
+ if intc.When != when {
+ continue
+ }
+
+ level, target := findTarget(val, intc.target)
+ if intc.interceptAll || target.IsValid() {
+ result = append(result, &interceptorItem{intc, target, level})
+ }
+ }
+
+ // Before is deepest to highest
+ if when == BEFORE {
+ sort.Sort(result)
+ } else {
+ // Everything else is highest to deepest
+ sort.Sort(reverseInterceptorItemList(result))
+ }
+ return result
+}
+
+// Find the value of the target, starting from val and including embedded types.
+// Also, convert between any difference in indirection.
+// If the target couldn't be found, the returned Value will have IsValid() == false
+func findTarget(val reflect.Value, target reflect.Type) (int, reflect.Value) {
+ // Look through the embedded types (until we reach the *revel.Controller at the top).
+ valueQueue := []reflect.Value{val}
+ level := 0
+ for len(valueQueue) > 0 {
+ val, valueQueue = valueQueue[0], valueQueue[1:]
+
+ // Check if val is of a similar type to the target type.
+ if val.Type() == target {
+ return level, val
+ }
+ if val.Kind() == reflect.Ptr && val.Elem().Type() == target {
+ return level, val.Elem()
+ }
+ if target.Kind() == reflect.Ptr && target.Elem() == val.Type() {
+ return level, val.Addr()
+ }
+
+ // If we reached the *revel.Controller and still didn't find what we were
+ // looking for, give up.
+ if val.Type() == controllerPtrType {
+ continue
+ }
+
+ // Else, add each anonymous field to the queue.
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+
+ for i := 0; i < val.NumField(); i++ {
+ if val.Type().Field(i).Anonymous {
+ valueQueue = append(valueQueue, val.Field(i))
+ }
+ }
+ level--
+ }
+
+ return level, reflect.Value{}
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "reflect"
+ "testing"
+)
+
+var funcP = func(c *Controller) Result { return nil }
+var funcP2 = func(c *Controller) Result { return nil }
+
+type InterceptController struct{ *Controller }
+type InterceptControllerN struct{ InterceptController }
+type InterceptControllerP struct{ *InterceptController }
+type InterceptControllerNP struct {
+ *Controller
+ InterceptControllerN
+ InterceptControllerP
+}
+
+func (c InterceptController) methN() Result { return nil }
+func (c *InterceptController) methP() Result { return nil }
+
+func (c InterceptControllerN) methNN() Result { return nil }
+func (c *InterceptControllerN) methNP() Result { return nil }
+func (c InterceptControllerP) methPN() Result { return nil }
+func (c *InterceptControllerP) methPP() Result { return nil }
+
+// Methods accessible from InterceptControllerN
+var MethodN = []interface{}{
+ InterceptController.methN,
+ (*InterceptController).methP,
+ InterceptControllerN.methNN,
+ (*InterceptControllerN).methNP,
+}
+
+// Methods accessible from InterceptControllerP
+var MethodP = []interface{}{
+ InterceptController.methN,
+ (*InterceptController).methP,
+ InterceptControllerP.methPN,
+ (*InterceptControllerP).methPP,
+}
+
+// This checks that all the various kinds of interceptor functions/methods are
+// properly invoked.
+func TestInvokeArgType(t *testing.T) {
+ n := InterceptControllerN{InterceptController{&Controller{}}}
+ p := InterceptControllerP{&InterceptController{&Controller{}}}
+ np := InterceptControllerNP{&Controller{}, n, p}
+ testInterceptorController(t, reflect.ValueOf(&n), MethodN)
+ testInterceptorController(t, reflect.ValueOf(&p), MethodP)
+ testInterceptorController(t, reflect.ValueOf(&np), MethodN)
+ testInterceptorController(t, reflect.ValueOf(&np), MethodP)
+}
+
+func testInterceptorController(t *testing.T, appControllerPtr reflect.Value, methods []interface{}) {
+ interceptors = []*Interception{}
+ InterceptFunc(funcP, BEFORE, appControllerPtr.Elem().Interface())
+ InterceptFunc(funcP2, BEFORE, AllControllers)
+ for _, m := range methods {
+ InterceptMethod(m, BEFORE)
+ }
+ ints := getInterceptors(BEFORE, appControllerPtr)
+
+ if len(ints) != 6 {
+ t.Fatalf("N: Expected 6 interceptors, got %d.", len(ints))
+ }
+
+ testInterception(t, ints[0], reflect.ValueOf(&Controller{}))
+ testInterception(t, ints[1], reflect.ValueOf(&Controller{}))
+ for i := range methods {
+ testInterception(t, ints[i+2], appControllerPtr)
+ }
+}
+
+func testInterception(t *testing.T, intc *interceptorItem, arg reflect.Value) {
+ val := intc.Interceptor.Invoke(arg, &intc.Target)
+ if !val.IsNil() {
+ t.Errorf("Failed (%v): Expected nil got %v", intc, val)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "io"
+ "reflect"
+)
+
+var (
+ controllerPtrType = reflect.TypeOf(&Controller{})
+ websocketType = reflect.TypeOf((*ServerWebSocket)(nil)).Elem()
+)
+
+func ActionInvoker(c *Controller, _ []Filter) {
+ // Instantiate the method.
+ methodValue := reflect.ValueOf(c.AppController).MethodByName(c.MethodType.Name)
+
+ // Collect the values for the method's arguments.
+ var methodArgs []reflect.Value
+ for _, arg := range c.MethodType.Args {
+ // If they accept a websocket connection, treat that arg specially.
+ var boundArg reflect.Value
+ if arg.Type.Implements(websocketType) {
+ boundArg = reflect.ValueOf(c.Request.WebSocket)
+ } else {
+ boundArg = Bind(c.Params, arg.Name, arg.Type)
+ // #756 - If the argument is a closer, defer a Close call,
+ // so we don't risk on leaks.
+ if closer, ok := boundArg.Interface().(io.Closer); ok {
+ defer func() {
+ _ = closer.Close()
+ }()
+ }
+ }
+ methodArgs = append(methodArgs, boundArg)
+ }
+
+ var resultValue reflect.Value
+ if methodValue.Type().IsVariadic() {
+ resultValue = methodValue.CallSlice(methodArgs)[0]
+ } else {
+ resultValue = methodValue.Call(methodArgs)[0]
+ }
+ if resultValue.Kind() == reflect.Interface && !resultValue.IsNil() {
+ c.Result = resultValue.Interface().(Result)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+// These tests verify that Controllers are initialized properly, given the range
+// of embedding possibilities..
+
+type P struct{ *Controller }
+
+type PN struct{ P }
+
+type PNN struct{ PN }
+
+// Embedded via two paths
+type P2 struct{ *Controller }
+type PP2 struct {
+ *Controller // Need to embed this explicitly to avoid duplicate selector.
+ P
+ P2
+ PNN
+}
+
+func TestFindControllers(t *testing.T) {
+ controllers = make(map[string]*ControllerType)
+ RegisterController((*P)(nil), nil)
+ RegisterController((*PN)(nil), nil)
+ RegisterController((*PNN)(nil), nil)
+ RegisterController((*PP2)(nil), nil)
+
+ // Test construction of indexes to each *Controller
+ checkSearchResults(t, P{}, [][]int{{0}})
+ checkSearchResults(t, PN{}, [][]int{{0, 0}})
+ checkSearchResults(t, PNN{}, [][]int{{0, 0, 0}})
+ checkSearchResults(t, PP2{}, [][]int{{0}, {1, 0}, {2, 0}, {3, 0, 0, 0}})
+}
+
+func checkSearchResults(t *testing.T, obj interface{}, expected [][]int) {
+ actual := findControllers(reflect.TypeOf(obj))
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Indexes do not match. expected %v actual %v", expected, actual)
+ }
+}
+
+func TestSetAction(t *testing.T) {
+ controllers = make(map[string]*ControllerType)
+ RegisterController((*P)(nil), []*MethodType{{Name: "Method"}})
+ RegisterController((*PNN)(nil), []*MethodType{{Name: "Method"}})
+ RegisterController((*PP2)(nil), []*MethodType{{Name: "Method"}})
+
+ // Test that all *revel.Controllers are initialized.
+ c := &Controller{Name: "Test"}
+ if err := c.SetAction("P", "Method"); err != nil {
+ t.Error(err)
+ } else if c.AppController.(*P).Controller != c {
+ t.Errorf("P not initialized")
+ }
+
+ if err := c.SetAction("PNN", "Method"); err != nil {
+ t.Error(err)
+ } else if c.AppController.(*PNN).Controller != c {
+ t.Errorf("PNN not initialized")
+ }
+
+ // PP2 has 4 different slots for *Controller.
+ if err := c.SetAction("PP2", "Method"); err != nil {
+ t.Error(err)
+ } else if pp2 := c.AppController.(*PP2); pp2.Controller != c ||
+ pp2.P.Controller != c ||
+ pp2.P2.Controller != c ||
+ pp2.PNN.Controller != c {
+ t.Errorf("PP2 not initialized")
+ }
+}
+
+func BenchmarkSetAction(b *testing.B) {
+ type Mixin1 struct {
+ *Controller
+ x, y int
+ foo string
+ }
+ type Mixin2 struct {
+ *Controller
+ a, b float64
+ bar string
+ }
+
+ type Benchmark struct {
+ *Controller
+ Mixin1
+ Mixin2
+ user interface{}
+ guy string
+ }
+
+ RegisterController((*Mixin1)(nil), []*MethodType{{Name: "Method"}})
+ RegisterController((*Mixin2)(nil), []*MethodType{{Name: "Method"}})
+ RegisterController((*Benchmark)(nil), []*MethodType{{Name: "Method"}})
+ c := Controller{
+ ViewArgs: make(map[string]interface{}),
+ }
+
+ for i := 0; i < b.N; i++ {
+ if err := c.SetAction("Benchmark", "Method"); err != nil {
+ b.Errorf("Failed to set action: %s", err)
+ return
+ }
+ }
+}
+
+func BenchmarkInvoker(b *testing.B) {
+ startFakeBookingApp()
+ c := NewTestController(nil, showRequest)
+ c.ViewArgs = make(map[string]interface{})
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ b.Errorf("Failed to set action: %s", err)
+ return
+ }
+
+ c.Params = &Params{Values: make(url.Values)}
+ c.Params.Set("id", "3")
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ ActionInvoker(c, nil)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/hex"
+ "io"
+ "reflect"
+ "strings"
+)
+
+// Sign a given string with the app-configured secret key.
+// If no secret key is set, returns the empty string.
+// Return the signature in base64 (URLEncoding).
+func Sign(message string) string {
+ if len(secretKey) == 0 {
+ return ""
+ }
+ mac := hmac.New(sha1.New, secretKey)
+ if _, err := io.WriteString(mac, message); err != nil {
+ utilLog.Error("WriteString failed", "error", err)
+ return ""
+ }
+ return hex.EncodeToString(mac.Sum(nil))
+}
+
+// Verify returns true if the given signature is correct for the given message.
+// e.g. it matches what we generate with Sign()
+func Verify(message, sig string) bool {
+ return hmac.Equal([]byte(sig), []byte(Sign(message)))
+}
+
+// ToBool method converts/assert value into true or false. Default is true.
+// When converting to boolean, the following values are considered FALSE:
+// - The integer value is 0 (zero)
+// - The float value 0.0 (zero)
+// - The complex value 0.0 (zero)
+// - For string value, please refer `revel.Atob` method
+// - An array, map, slice with zero elements
+// - Boolean value returned as-is
+// - "nil" value
+func ToBool(val interface{}) bool {
+ if val == nil {
+ return false
+ }
+
+ v := reflect.ValueOf(val)
+ switch v.Kind() {
+ case reflect.Bool:
+ return v.Bool()
+ case reflect.String:
+ return Atob(v.String())
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() != 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() != 0.0
+ case reflect.Complex64, reflect.Complex128:
+ return v.Complex() != 0.0
+ case reflect.Array, reflect.Map, reflect.Slice:
+ return v.Len() != 0
+ }
+
+ // Return true by default
+ return true
+}
+
+// Atob converts string into boolean. It is in-case sensitive
+// When converting to boolean, the following values are considered FALSE:
+// - The "" (empty) string
+// - The "false" string
+// - The "f" string
+// - The "off" string,
+// - The string "0" & "0.0"
+func Atob(v string) bool {
+ switch strings.TrimSpace(strings.ToLower(v)) {
+ case "", "false", "off", "f", "0", "0.0":
+ return false
+ }
+
+ // Return true by default
+ return true
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import "testing"
+
+func TestToBooleanForFalse(t *testing.T) {
+ if ToBool(nil) ||
+ ToBool([]string{}) ||
+ ToBool(map[string]string{}) ||
+ ToBool(0) ||
+ ToBool(0.0) ||
+ ToBool("") ||
+ ToBool("false") ||
+ ToBool("0") ||
+ ToBool("0.0") ||
+ ToBool("off") ||
+ ToBool("f") {
+ t.Error("Expected 'false' got 'true'")
+ }
+}
+
+func TestToBooleanForTrue(t *testing.T) {
+ if !ToBool([]string{"true"}) ||
+ !ToBool(map[string]string{"true": "value"}) ||
+ !ToBool(1) ||
+ !ToBool(0.1) ||
+ !ToBool("not empty") ||
+ !ToBool("true") ||
+ !ToBool("1") ||
+ !ToBool("1.0") ||
+ !ToBool("on") ||
+ !ToBool("t") {
+ t.Error("Expected 'true' got 'false'")
+ }
+}
--- /dev/null
+package revel
+
+import (
+ "github.com/revel/revel/logger"
+)
+
+//Logger
+var (
+ // The root log is what all other logs are branched from, meaning if you set the handler for the root
+ // it will adjust all children
+ RootLog = logger.New()
+ // This logger is the application logger, use this for your application log messages - ie jobs and startup,
+ // Use Controller.Log for Controller logging
+ // The requests are logged to this logger with the context of `section:requestlog`
+ AppLog = RootLog.New("module", "app")
+ // This is the logger revel writes to, added log messages will have a context of module:revel in them
+ // It is based off of `RootLog`
+ RevelLog = RootLog.New("module", "revel")
+
+ // This is the handler for the AppLog, it is stored so that if the AppLog is changed it can be assigned to the
+ // new AppLog
+ appLogHandler *logger.CompositeMultiHandler
+
+ // This oldLog is the revel logger, historical for revel, The application should use the AppLog or the Controller.oldLog
+ // DEPRECATED
+ oldLog = AppLog.New("section", "deprecated")
+ // System logger
+ SysLog = AppLog.New("section", "system")
+)
+
+// Initialize the loggers first
+func init() {
+
+ //RootLog.SetHandler(
+ // logger.LevelHandler(logger.LogLevel(log15.LvlDebug),
+ // logger.StreamHandler(os.Stdout, logger.TerminalFormatHandler(false, true))))
+ initLoggers()
+ OnAppStart(initLoggers, -5)
+
+}
+func initLoggers() {
+ appHandle := logger.InitializeFromConfig(BasePath, Config)
+
+ // Set all the log handlers
+ setAppLog(AppLog, appHandle)
+}
+
+// Set the application log and handler, if handler is nil it will
+// use the same handler used to configure the application log before
+func setAppLog(appLog logger.MultiLogger, appHandler *logger.CompositeMultiHandler) {
+ if appLog != nil {
+ AppLog = appLog
+ }
+ if appHandler != nil {
+ appLogHandler = appHandler
+ // Set the app log and the handler for all forked loggers
+ RootLog.SetHandler(appLogHandler)
+
+ // Set the system log handler - this sets golang writer stream to the
+ // sysLog router
+ logger.SetDefaultLog(SysLog)
+ SysLog.SetStackDepth(5)
+ SysLog.SetHandler(appLogHandler)
+ }
+}
--- /dev/null
+package logger
+
+import (
+ "github.com/mattn/go-colorable"
+ "gopkg.in/natefinch/lumberjack.v2"
+ "io"
+ "os"
+)
+
+type CompositeMultiHandler struct {
+ DebugHandler LogHandler
+ InfoHandler LogHandler
+ WarnHandler LogHandler
+ ErrorHandler LogHandler
+ CriticalHandler LogHandler
+}
+
+func NewCompositeMultiHandler() (*CompositeMultiHandler, LogHandler) {
+ cw := &CompositeMultiHandler{}
+ return cw, cw
+}
+func (h *CompositeMultiHandler) Log(r *Record) (err error) {
+
+ var handler LogHandler
+
+ switch r.Level {
+ case LvlInfo:
+ handler = h.InfoHandler
+ case LvlDebug:
+ handler = h.DebugHandler
+ case LvlWarn:
+ handler = h.WarnHandler
+ case LvlError:
+ handler = h.ErrorHandler
+ case LvlCrit:
+ handler = h.CriticalHandler
+ }
+
+ // Embed the caller function in the context
+ if handler != nil {
+ handler.Log(r)
+ }
+ return
+}
+
+func (h *CompositeMultiHandler) SetHandler(handler LogHandler, replace bool, level LogLevel) {
+ if handler == nil {
+ // Ignore empty handler
+ return
+ }
+ source := &h.DebugHandler
+ switch level {
+ case LvlDebug:
+ source = &h.DebugHandler
+ case LvlInfo:
+ source = &h.InfoHandler
+ case LvlWarn:
+ source = &h.WarnHandler
+ case LvlError:
+ source = &h.ErrorHandler
+ case LvlCrit:
+ source = &h.CriticalHandler
+ }
+
+ if !replace && *source != nil {
+ // If we are not replacing the source make sure that the level handler is applied first
+ if _, isLevel := (*source).(*LevelFilterHandler); !isLevel {
+ *source = LevelHandler(level, *source)
+ }
+ // If this already was a list add a new logger to it
+ if ll, found := (*source).(*ListLogHandler); found {
+ ll.Add(handler)
+ } else {
+ *source = NewListLogHandler(*source, handler)
+ }
+ } else {
+ *source = handler
+ }
+}
+
+// For the multi handler set the handler, using the LogOptions defined
+func (h *CompositeMultiHandler) SetHandlers(handler LogHandler, options *LogOptions) {
+ if len(options.Levels) == 0 {
+ options.Levels = LvlAllList
+ }
+
+ // Set all levels
+ for _, lvl := range options.Levels {
+ h.SetHandler(handler, options.ReplaceExistingHandler, lvl)
+ }
+
+}
+func (h *CompositeMultiHandler) SetJson(writer io.Writer, options *LogOptions) {
+ handler := CallerFileHandler(StreamHandler(writer, JsonFormatEx(
+ options.GetBoolDefault("pretty", false),
+ options.GetBoolDefault("lineSeparated", true),
+ )))
+ if options.HandlerWrap != nil {
+ handler = options.HandlerWrap.SetChild(handler)
+ }
+ h.SetHandlers(handler, options)
+}
+
+// Use built in rolling function
+func (h *CompositeMultiHandler) SetJsonFile(filePath string, options *LogOptions) {
+ writer := &lumberjack.Logger{
+ Filename: filePath,
+ MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes
+ MaxAge: options.GetIntDefault("maxAgeDays", 7), //days
+ MaxBackups: options.GetIntDefault("maxBackups", 7),
+ Compress: options.GetBoolDefault("compress", true),
+ }
+ h.SetJson(writer, options)
+}
+
+func (h *CompositeMultiHandler) SetTerminal(writer io.Writer, options *LogOptions) {
+ streamHandler := StreamHandler(
+ writer,
+ TerminalFormatHandler(
+ options.GetBoolDefault("noColor", false),
+ options.GetBoolDefault("smallDate", true)))
+
+ if os.Stdout == writer {
+ streamHandler = StreamHandler(
+ colorable.NewColorableStdout(),
+ TerminalFormatHandler(
+ options.GetBoolDefault("noColor", false),
+ options.GetBoolDefault("smallDate", true)))
+ } else if os.Stderr == writer {
+ streamHandler = StreamHandler(
+ colorable.NewColorableStderr(),
+ TerminalFormatHandler(
+ options.GetBoolDefault("noColor", false),
+ options.GetBoolDefault("smallDate", true)))
+ }
+ handler := CallerFileHandler(streamHandler)
+
+ if options.HandlerWrap != nil {
+ handler = options.HandlerWrap.SetChild(handler)
+ }
+ h.SetHandlers(handler, options)
+}
+
+// Use built in rolling function
+func (h *CompositeMultiHandler) SetTerminalFile(filePath string, options *LogOptions) {
+ writer := &lumberjack.Logger{
+ Filename: filePath,
+ MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes
+ MaxAge: options.GetIntDefault("maxAgeDays", 7), //days
+ MaxBackups: options.GetIntDefault("maxBackups", 7),
+ Compress: options.GetBoolDefault("compress", true),
+ }
+ h.SetTerminal(writer, options)
+}
+
+func (h *CompositeMultiHandler) Disable(levels ...LogLevel) {
+ if len(levels) == 0 {
+ levels = LvlAllList
+ }
+ for _, level := range levels {
+ switch level {
+ case LvlDebug:
+ h.DebugHandler = nil
+ case LvlInfo:
+ h.InfoHandler = nil
+ case LvlWarn:
+ h.WarnHandler = nil
+ case LvlError:
+ h.ErrorHandler = nil
+ case LvlCrit:
+ h.CriticalHandler = nil
+ }
+ }
+}
--- /dev/null
+/*
+ Package logger contains filters and handles for the logging utilities in Revel.
+ These facilities all currently use the logging library called log15 at
+ https://github.com/inconshreveable/log15
+
+ Defining handlers happens as follows
+ 1) ALL handlers (log.all.output) replace any existing handlers
+ 2) Output handlers (log.error.output) replace any existing handlers
+ 3) Filter handlers (log.xxx.filter, log.xxx.nfilter) append to existing handlers,
+ note log.all.filter is treated as a filter handler, so it will NOT replace existing ones
+
+
+
+*/
+package logger
--- /dev/null
+package logger
+
+import (
+ "fmt"
+ "io"
+)
+
+type LevelFilterHandler struct {
+ Level LogLevel
+ h LogHandler
+}
+
+// Filters out records which do not match the level
+// Uses the `log15.FilterHandler` to perform this task
+func LevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+ return &LevelFilterHandler{lvl, h}
+}
+
+// The implementation of the Log
+func (h LevelFilterHandler) Log(r *Record) error {
+ if r.Level == h.Level {
+ return h.h.Log(r)
+ }
+ return nil
+}
+
+// Filters out records which do not match the level
+// Uses the `log15.FilterHandler` to perform this task
+func MinLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+ return FilterHandler(func(r *Record) (pass bool) {
+ return r.Level <= lvl
+ }, h)
+}
+
+// Filters out records which match the level
+// Uses the `log15.FilterHandler` to perform this task
+func NotLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+ return FilterHandler(func(r *Record) (pass bool) {
+ return r.Level != lvl
+ }, h)
+}
+
+func CallerFileHandler(h LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ r.Context.Add("caller", fmt.Sprint(r.Call))
+ return h.Log(r)
+ })
+}
+
+// Adds in a context called `caller` to the record (contains file name and line number like `foo.go:12`)
+// Uses the `log15.CallerFuncHandler` to perform this task
+func CallerFuncHandler(h LogHandler) LogHandler {
+ return CallerFuncHandler(h)
+}
+
+// Filters out records which match the key value pair
+// Uses the `log15.MatchFilterHandler` to perform this task
+func MatchHandler(key string, value interface{}, h LogHandler) LogHandler {
+ return MatchFilterHandler(key, value, h)
+}
+
+// MatchFilterHandler returns a Handler that only writes records
+// to the wrapped Handler if the given key in the logged
+// context matches the value. For example, to only log records
+// from your ui package:
+//
+// log.MatchFilterHandler("pkg", "app/ui", log.StdoutHandler)
+//
+func MatchFilterHandler(key string, value interface{}, h LogHandler) LogHandler {
+ return FilterHandler(func(r *Record) (pass bool) {
+ return r.Context[key] == value
+ }, h)
+}
+
+// If match then A handler is called otherwise B handler is called
+func MatchAbHandler(key string, value interface{}, a, b LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ if r.Context[key] == value {
+ return a.Log(r)
+ } else if b != nil {
+ return b.Log(r)
+ }
+
+ return nil
+ })
+}
+
+// The nil handler is used if logging for a specific request needs to be turned off
+func NilHandler() LogHandler {
+ return FuncHandler(func(r *Record) error {
+ return nil
+ })
+}
+
+// Match all values in map to log
+func MatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
+ return matchMapHandler(matchMap, false, a)
+}
+
+// Match !(Match all values in map to log) The inverse of MatchMapHandler
+func NotMatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
+ return matchMapHandler(matchMap, true, a)
+}
+
+// Rather then chaining multiple filter handlers, process all here
+func matchMapHandler(matchMap map[string]interface{}, inverse bool, a LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ matchCount := 0
+ for k, v := range matchMap {
+ value, found := r.Context[k]
+ if !found {
+ return nil
+ }
+ // Test for two failure cases
+ if value == v && inverse || value != v && !inverse {
+ return nil
+ } else {
+ matchCount++
+ }
+ }
+ if matchCount != len(matchMap) {
+ return nil
+ }
+ return a.Log(r)
+ })
+}
+
+// Filters out records which do not match the key value pair
+// Uses the `log15.FilterHandler` to perform this task
+func NotMatchHandler(key string, value interface{}, h LogHandler) LogHandler {
+ return FilterHandler(func(r *Record) (pass bool) {
+ return r.Context[key] != value
+ }, h)
+}
+
+func MultiHandler(hs ...LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ for _, h := range hs {
+ // what to do about failures?
+ h.Log(r)
+ }
+ return nil
+ })
+}
+
+// StreamHandler writes log records to an io.Writer
+// with the given format. StreamHandler can be used
+// to easily begin writing log records to other
+// outputs.
+//
+// StreamHandler wraps itself with LazyHandler and SyncHandler
+// to evaluate Lazy objects and perform safe concurrent writes.
+func StreamHandler(wr io.Writer, fmtr LogFormat) LogHandler {
+ h := FuncHandler(func(r *Record) error {
+ _, err := wr.Write(fmtr.Format(r))
+ return err
+ })
+ return LazyHandler(SyncHandler(h))
+}
+
+// Filter handler
+func FilterHandler(fn func(r *Record) bool, h LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ if fn(r) {
+ return h.Log(r)
+ }
+ return nil
+ })
+}
+
+// List log handler handles a list of LogHandlers
+type ListLogHandler struct {
+ handlers []LogHandler
+}
+
+// Create a new list of log handlers
+func NewListLogHandler(h1, h2 LogHandler) *ListLogHandler {
+ ll := &ListLogHandler{handlers: []LogHandler{h1, h2}}
+ return ll
+}
+
+// Log the record
+func (ll *ListLogHandler) Log(r *Record) (err error) {
+ for _, handler := range ll.handlers {
+ if err == nil {
+ err = handler.Log(r)
+ } else {
+ handler.Log(r)
+ }
+ }
+ return
+}
+
+// Add another log handler
+func (ll *ListLogHandler) Add(h LogHandler) {
+ if h != nil {
+ ll.handlers = append(ll.handlers, h)
+ }
+}
+
+// Remove a log handler
+func (ll *ListLogHandler) Del(h LogHandler) {
+ if h != nil {
+ for i, handler := range ll.handlers {
+ if handler == h {
+ ll.handlers = append(ll.handlers[:i], ll.handlers[i+1:]...)
+ }
+ }
+ }
+}
--- /dev/null
+package logger
+
+// Get all handlers based on the Config (if available)
+import (
+ "fmt"
+ "github.com/revel/config"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func InitializeFromConfig(basePath string, config *config.Context) (c *CompositeMultiHandler) {
+ // If running in test mode suppress anything that is not an error
+ if config != nil && config.BoolDefault(TEST_MODE_FLAG, false) {
+ // Preconfigure all the options
+ config.SetOption("log.info.output", "none")
+ config.SetOption("log.debug.output", "none")
+ config.SetOption("log.warn.output", "none")
+ config.SetOption("log.error.output", "stderr")
+ config.SetOption("log.crit.output", "stderr")
+ }
+
+ // If the configuration has an all option we can skip some
+ c, _ = NewCompositeMultiHandler()
+
+ // Filters are assigned first, non filtered items override filters
+ if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
+ initAllLog(c, basePath, config)
+ }
+ initLogLevels(c, basePath, config)
+ if c.CriticalHandler == nil && c.ErrorHandler != nil {
+ c.CriticalHandler = c.ErrorHandler
+ }
+ if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
+ initFilterLog(c, basePath, config)
+ if c.CriticalHandler == nil && c.ErrorHandler != nil {
+ c.CriticalHandler = c.ErrorHandler
+ }
+ initRequestLog(c, basePath, config)
+ }
+
+ return c
+}
+
+// Init the log.all configuration options
+func initAllLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+ if config != nil {
+ extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+ if output, found := config.String("log.all.output"); found {
+ // Set all output for the specified handler
+ if extraLogFlag {
+ log.Printf("Adding standard handler for levels to >%s< ", output)
+ }
+ initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, LvlAllList...))
+ }
+ }
+}
+
+// Init the filter options
+// log.all.filter ....
+// log.error.filter ....
+func initFilterLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+
+ if config != nil {
+ extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+
+ for _, logFilter := range logFilterList {
+ // Init for all filters
+ for _, name := range []string{"all", "debug", "info", "warn", "error", "crit",
+ "trace", // TODO trace is deprecated
+ } {
+ optionList := config.Options(logFilter.LogPrefix + name + logFilter.LogSuffix)
+ for _, option := range optionList {
+ splitOptions := strings.Split(option, ".")
+ keyMap := map[string]interface{}{}
+ for x := 3; x < len(splitOptions); x += 2 {
+ keyMap[splitOptions[x]] = splitOptions[x+1]
+ }
+ phandler := logFilter.parentHandler(keyMap)
+ if extraLogFlag {
+ log.Printf("Adding key map handler %s %s output %s", option, name, config.StringDefault(option, ""))
+ fmt.Printf("Adding key map handler %s %s output %s matching %#v\n", option, name, config.StringDefault(option, ""), keyMap)
+ }
+
+ if name == "all" {
+ initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler))
+ } else {
+ initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler, toLevel[name]))
+ }
+ }
+ }
+ }
+ }
+}
+
+// Init the log.error, log.warn etc configuration options
+func initLogLevels(c *CompositeMultiHandler, basePath string, config *config.Context) {
+ for _, name := range []string{"debug", "info", "warn", "error", "crit",
+ "trace", // TODO trace is deprecated
+ } {
+ if config != nil {
+ extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+ output, found := config.String("log." + name + ".output")
+ if found {
+ if extraLogFlag {
+ log.Printf("Adding standard handler %s output %s", name, output)
+ }
+ initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, toLevel[name]))
+ }
+ // Gets the list of options with said prefix
+ } else {
+ initHandlerFor(c, "stderr", basePath, NewLogOptions(config, true, nil, toLevel[name]))
+ }
+ }
+}
+
+// Init the request log options
+func initRequestLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+ // Request logging to a separate output handler
+ // This takes the InfoHandlers and adds a MatchAbHandler handler to it to direct
+ // context with the word "section=requestlog" to that handler.
+ // Note if request logging is not enabled the MatchAbHandler will not be added and the
+ // request log messages will be sent out the INFO handler
+ outputRequest := "stdout"
+ if config != nil {
+ outputRequest = config.StringDefault("log.request.output", "")
+ }
+ oldInfo := c.InfoHandler
+ c.InfoHandler = nil
+ if outputRequest != "" {
+ initHandlerFor(c, outputRequest, basePath, NewLogOptions(config, false, nil, LvlInfo))
+ }
+ if c.InfoHandler != nil || oldInfo != nil {
+ if c.InfoHandler == nil {
+ c.InfoHandler = oldInfo
+ } else {
+ c.InfoHandler = MatchAbHandler("section", "requestlog", c.InfoHandler, oldInfo)
+ }
+ }
+}
+
+// Returns a handler for the level using the output string
+// Accept formats for output string are
+// LogFunctionMap[value] callback function
+// `stdout` `stderr` `full/file/path/to/location/app.log` `full/file/path/to/location/app.json`
+func initHandlerFor(c *CompositeMultiHandler, output, basePath string, options *LogOptions) {
+ if options.Ctx != nil {
+ options.SetExtendedOptions(
+ "noColor", !options.Ctx.BoolDefault("log.colorize", true),
+ "smallDate", options.Ctx.BoolDefault("log.smallDate", true),
+ "maxSize", options.Ctx.IntDefault("log.maxsize", 1024*10),
+ "maxAge", options.Ctx.IntDefault("log.maxage", 14),
+ "maxBackups", options.Ctx.IntDefault("log.maxbackups", 14),
+ "compressBackups", !options.Ctx.BoolDefault("log.compressBackups", true),
+ )
+ }
+
+ output = strings.TrimSpace(output)
+ if funcHandler, found := LogFunctionMap[output]; found {
+ funcHandler(c, options)
+ } else {
+ switch output {
+ case "":
+ fallthrough
+ case "off":
+ // No handler, discard data
+ default:
+ // Write to file specified
+ if !filepath.IsAbs(output) {
+ output = filepath.Join(basePath, output)
+ }
+
+ if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
+ log.Panic(err)
+ }
+
+ if strings.HasSuffix(output, "json") {
+ c.SetJsonFile(output, options)
+ } else {
+ // Override defaults for a terminal file
+ options.SetExtendedOptions("noColor", true)
+ options.SetExtendedOptions("smallDate", false)
+ c.SetTerminalFile(output, options)
+ }
+ }
+ }
+ return
+}
--- /dev/null
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package logger_test
+
+import (
+ "github.com/revel/config"
+ "github.com/revel/revel/logger"
+ "github.com/stretchr/testify/assert"
+ "os"
+ "strings"
+ "testing"
+)
+
+type (
+ // A counter for the tester
+ testCounter struct {
+ debug, info, warn, error, critical int
+ }
+ // The data to tes
+ testData struct {
+ config []string
+ result testResult
+ tc *testCounter
+ }
+ // The test result
+ testResult struct {
+ debug, info, warn, error, critical int
+ }
+)
+
+// Single test cases
+var singleCases = []testData{
+ {config: []string{"log.crit.output"},
+ result: testResult{0, 0, 0, 0, 1}},
+ {config: []string{"log.error.output"},
+ result: testResult{0, 0, 0, 1, 1}},
+ {config: []string{"log.warn.output"},
+ result: testResult{0, 0, 1, 0, 0}},
+ {config: []string{"log.info.output"},
+ result: testResult{0, 1, 0, 0, 0}},
+ {config: []string{"log.debug.output"},
+ result: testResult{1, 0, 0, 0, 0}},
+}
+
+// Test singles
+func TestSingleCases(t *testing.T) {
+ rootLog := logger.New()
+ for _, testCase := range singleCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// Filter test cases
+var filterCases = []testData{
+ {config: []string{"log.crit.filter.module.app"},
+ result: testResult{0, 0, 0, 0, 1}},
+ {config: []string{"log.crit.filter.module.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.error.filter.module.app"},
+ result: testResult{0, 0, 0, 1, 1}},
+ {config: []string{"log.error.filter.module.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.warn.filter.module.app"},
+ result: testResult{0, 0, 1, 0, 0}},
+ {config: []string{"log.warn.filter.module.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.info.filter.module.app"},
+ result: testResult{0, 1, 0, 0, 0}},
+ {config: []string{"log.info.filter.module.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.debug.filter.module.app"},
+ result: testResult{1, 0, 0, 0, 0}},
+ {config: []string{"log.debug.filter.module.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+}
+
+// Filter test
+func TestFilterCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for _, testCase := range filterCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// Inverse test cases
+var nfilterCases = []testData{
+ {config: []string{"log.crit.nfilter.module.appa"},
+ result: testResult{0, 0, 0, 0, 1}},
+ {config: []string{"log.crit.nfilter.modules.appa"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.crit.nfilter.module.app"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.error.nfilter.module.appa"}, // Special case, when error is not nill critical inherits from error
+ result: testResult{0, 0, 0, 1, 1}},
+ {config: []string{"log.error.nfilter.module.app"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.warn.nfilter.module.appa"},
+ result: testResult{0, 0, 1, 0, 0}},
+ {config: []string{"log.warn.nfilter.module.app"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.info.nfilter.module.appa"},
+ result: testResult{0, 1, 0, 0, 0}},
+ {config: []string{"log.info.nfilter.module.app"},
+ result: testResult{0, 0, 0, 0, 0}},
+ {config: []string{"log.debug.nfilter.module.appa"},
+ result: testResult{1, 0, 0, 0, 0}},
+ {config: []string{"log.debug.nfilter.module.app"},
+ result: testResult{0, 0, 0, 0, 0}},
+}
+
+// Inverse test
+func TestNotFilterCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for _, testCase := range nfilterCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// off test cases
+var offCases = []testData{
+ {config: []string{"log.all.output", "log.error.output=off"},
+ result: testResult{1, 1, 1, 0, 1}},
+}
+
+// Off test
+func TestOffCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for _, testCase := range offCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// Duplicate test cases
+var duplicateCases = []testData{
+ {config: []string{"log.all.output", "log.error.output", "log.error.filter.module.app"},
+ result: testResult{1, 1, 1, 2, 1}},
+}
+
+// test duplicate cases
+func TestDuplicateCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for _, testCase := range duplicateCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// Contradicting cases
+var contradictCases = []testData{
+ {config: []string{"log.all.output", "log.error.output=off", "log.all.output"},
+ result: testResult{1, 1, 1, 0, 1}},
+ {config: []string{"log.all.output", "log.error.output=off", "log.debug.filter.module.app"},
+ result: testResult{2, 1, 1, 0, 1}},
+ {config: []string{"log.all.filter.module.app", "log.info.output=off", "log.info.filter.module.app"},
+ result: testResult{1, 2, 1, 1, 1}},
+ {config: []string{"log.all.output", "log.info.output=off", "log.info.filter.module.app"},
+ result: testResult{1, 1, 1, 1, 1}},
+}
+
+// Contradiction test
+func TestContradictCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for _, testCase := range contradictCases {
+ testCase.logTest(rootLog, t)
+ testCase.validate(t)
+ }
+}
+
+// All test cases
+var allCases = []testData{
+ {config: []string{"log.all.filter.module.app"},
+ result: testResult{1, 1, 1, 1, 1}},
+ {config: []string{"log.all.output"},
+ result: testResult{2, 2, 2, 2, 2}},
+}
+
+// All tests
+func TestAllCases(t *testing.T) {
+ rootLog := logger.New("module", "app")
+ for i, testCase := range allCases {
+ testCase.logTest(rootLog, t)
+ allCases[i] = testCase
+ }
+ rootLog = logger.New()
+ for i, testCase := range allCases {
+ testCase.logTest(rootLog, t)
+ allCases[i] = testCase
+ }
+ for _, testCase := range allCases {
+ testCase.validate(t)
+ }
+
+}
+
+func (c *testCounter) Log(r *logger.Record) error {
+ switch r.Level {
+ case logger.LvlDebug:
+ c.debug++
+ case logger.LvlInfo:
+ c.info++
+ case logger.LvlWarn:
+ c.warn++
+ case logger.LvlError:
+ c.error++
+ case logger.LvlCrit:
+ c.critical++
+ default:
+ panic("Unknown log level")
+ }
+ return nil
+}
+func (td *testData) logTest(rootLog logger.MultiLogger, t *testing.T) {
+ if td.tc == nil {
+ td.tc = &testCounter{}
+ counterInit(td.tc)
+ }
+ newContext := config.NewContext()
+ for _, i := range td.config {
+ iout := strings.Split(i, "=")
+ if len(iout) > 1 {
+ newContext.SetOption(iout[0], iout[1])
+ } else {
+ newContext.SetOption(i, "test")
+ }
+ }
+
+ newContext.SetOption("specialUseFlag", "true")
+
+ handler := logger.InitializeFromConfig("test", newContext)
+
+ rootLog.SetHandler(handler)
+
+ td.runLogTest(rootLog)
+}
+
+func (td *testData) runLogTest(log logger.MultiLogger) {
+ log.Debug("test")
+ log.Info("test")
+ log.Warn("test")
+ log.Error("test")
+ log.Crit("test")
+}
+
+func (td *testData) validate(t *testing.T) {
+ t.Logf("Test %#v expected %#v", td.tc, td.result)
+ assert.Equal(t, td.result.debug, td.tc.debug, "Debug failed "+strings.Join(td.config, " "))
+ assert.Equal(t, td.result.info, td.tc.info, "Info failed "+strings.Join(td.config, " "))
+ assert.Equal(t, td.result.warn, td.tc.warn, "Warn failed "+strings.Join(td.config, " "))
+ assert.Equal(t, td.result.error, td.tc.error, "Error failed "+strings.Join(td.config, " "))
+ assert.Equal(t, td.result.critical, td.tc.critical, "Critical failed "+strings.Join(td.config, " "))
+}
+
+// Add test to the function map
+func counterInit(tc *testCounter) {
+ logger.LogFunctionMap["test"] = func(c *logger.CompositeMultiHandler, logOptions *logger.LogOptions) {
+ // Output to the test log and the stdout
+ outHandler := logger.LogHandler(
+ logger.NewListLogHandler(tc,
+ logger.StreamHandler(os.Stdout, logger.TerminalFormatHandler(false, true))),
+ )
+ if logOptions.HandlerWrap != nil {
+ outHandler = logOptions.HandlerWrap.SetChild(outHandler)
+ }
+
+ c.SetHandlers(outHandler, logOptions)
+ }
+}
--- /dev/null
+package logger
+
+import (
+ "os"
+)
+
+// The log function map can be added to, so that you can specify your own logging mechanism
+// it has defaults for off, stdout, stderr
+var LogFunctionMap = map[string]func(*CompositeMultiHandler, *LogOptions){
+ // Do nothing - set the logger off
+ "off": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+ // Only drop the results if there is a parent handler defined
+ if logOptions.HandlerWrap != nil {
+ for _, l := range logOptions.Levels {
+ c.SetHandler(logOptions.HandlerWrap.SetChild(NilHandler()), logOptions.ReplaceExistingHandler, l)
+ }
+ } else {
+ // Clear existing handler
+ c.SetHandlers(NilHandler(), logOptions)
+ }
+ },
+ // Do nothing - set the logger off
+ "": func(*CompositeMultiHandler, *LogOptions) {},
+ // Set the levels to stdout, replace existing
+ "stdout": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+ if logOptions.Ctx != nil {
+ logOptions.SetExtendedOptions(
+ "noColor", !logOptions.Ctx.BoolDefault("log.colorize", true),
+ "smallDate", logOptions.Ctx.BoolDefault("log.smallDate", true))
+ }
+ c.SetTerminal(os.Stdout, logOptions)
+ },
+ // Set the levels to stderr output to terminal
+ "stderr": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+ c.SetTerminal(os.Stderr, logOptions)
+ },
+}
--- /dev/null
+package logger
+
+import (
+ "fmt"
+ "github.com/revel/config"
+ "time"
+)
+
+// The LogHandler defines the interface to handle the log records
+type (
+ // The Multilogger reduces the number of exposed defined logging variables,
+ // and allows the output to be easily refined
+ MultiLogger interface {
+ // New returns a new Logger that has this logger's context plus the given context
+ New(ctx ...interface{}) MultiLogger
+
+ // SetHandler updates the logger to write records to the specified handler.
+ SetHandler(h LogHandler)
+
+ // Set the stack depth for the logger
+ SetStackDepth(int) MultiLogger
+
+ // Log a message at the given level with context key/value pairs
+ Debug(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters
+ Debugf(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs
+ Info(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters
+ Infof(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs
+ Warn(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters
+ Warnf(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs
+ Error(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters
+ Errorf(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs
+ Crit(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters
+ Critf(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs and exits
+ Fatal(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters and exits
+ Fatalf(msg string, params ...interface{})
+
+ // Log a message at the given level with context key/value pairs and panics
+ Panic(msg string, ctx ...interface{})
+
+ // Log a message at the given level formatting message with the parameters and panics
+ Panicf(msg string, params ...interface{})
+ }
+
+ // The log handler interface
+ LogHandler interface {
+ Log(*Record) error
+ //log15.Handler
+ }
+
+ // The log stack handler interface
+ LogStackHandler interface {
+ LogHandler
+ GetStack() int
+ }
+
+ // The log handler interface which has child logs
+ ParentLogHandler interface {
+ SetChild(handler LogHandler) LogHandler
+ }
+
+ // The log format interface
+ LogFormat interface {
+ Format(r *Record) []byte
+ }
+
+ // The log level type
+ LogLevel int
+
+ // Used for the callback to LogFunctionMap
+ LogOptions struct {
+ Ctx *config.Context
+ ReplaceExistingHandler bool
+ HandlerWrap ParentLogHandler
+ Levels []LogLevel
+ ExtendedOptions map[string]interface{}
+ }
+
+ // The log record
+ Record struct {
+ Message string // The message
+ Time time.Time // The time
+ Level LogLevel //The level
+ Call CallStack // The call stack if built
+ Context ContextMap // The context
+ }
+
+ // The lazy structure to implement a function to be invoked only if needed
+ Lazy struct {
+ Fn interface{} // the function
+ }
+
+ // Currently the only requirement for the callstack is to support the Formatter method
+ // which stack.Call does so we use that
+ CallStack interface {
+ fmt.Formatter // Requirement
+ }
+)
+
+// FormatFunc returns a new Format object which uses
+// the given function to perform record formatting.
+func FormatFunc(f func(*Record) []byte) LogFormat {
+ return formatFunc(f)
+}
+
+type formatFunc func(*Record) []byte
+
+func (f formatFunc) Format(r *Record) []byte {
+ return f(r)
+}
+func NewRecord(message string, level LogLevel) *Record {
+ return &Record{Message: message, Context: ContextMap{}, Level: level}
+}
+
+const (
+ LvlCrit LogLevel = iota // Critical
+ LvlError // Error
+ LvlWarn // Warning
+ LvlInfo // Information
+ LvlDebug // Debug
+)
+
+// A list of all the log levels
+var LvlAllList = []LogLevel{LvlDebug, LvlInfo, LvlWarn, LvlError, LvlCrit}
+
+// Implements the ParentLogHandler
+type parentLogHandler struct {
+ setChild func(handler LogHandler) LogHandler
+}
+
+// Create a new parent log handler
+func NewParentLogHandler(callBack func(child LogHandler) LogHandler) ParentLogHandler {
+ return &parentLogHandler{callBack}
+}
+
+// Sets the child of the log handler
+func (p *parentLogHandler) SetChild(child LogHandler) LogHandler {
+ return p.setChild(child)
+}
+
+// Create a new log options
+func NewLogOptions(cfg *config.Context, replaceHandler bool, phandler ParentLogHandler, lvl ...LogLevel) (logOptions *LogOptions) {
+ logOptions = &LogOptions{
+ Ctx: cfg,
+ ReplaceExistingHandler: replaceHandler,
+ HandlerWrap: phandler,
+ Levels: lvl,
+ ExtendedOptions: map[string]interface{}{},
+ }
+ return
+}
+
+// Assumes options will be an even number and have a string, value syntax
+func (l *LogOptions) SetExtendedOptions(options ...interface{}) {
+ for x := 0; x < len(options); x += 2 {
+ l.ExtendedOptions[options[x].(string)] = options[x+1]
+ }
+}
+
+// Gets a string option with default
+func (l *LogOptions) GetStringDefault(option, value string) string {
+ if v, found := l.ExtendedOptions[option]; found {
+ return v.(string)
+ }
+ return value
+}
+
+// Gets an int option with default
+func (l *LogOptions) GetIntDefault(option string, value int) int {
+ if v, found := l.ExtendedOptions[option]; found {
+ return v.(int)
+ }
+ return value
+}
+
+// Gets a boolean option with default
+func (l *LogOptions) GetBoolDefault(option string, value bool) bool {
+ if v, found := l.ExtendedOptions[option]; found {
+ return v.(bool)
+ }
+ return value
+}
--- /dev/null
+package logger
+
+import (
+ "fmt"
+ "github.com/revel/log15"
+ "log"
+ "os"
+)
+
+// This type implements the MultiLogger
+type RevelLogger struct {
+ log15.Logger
+}
+
+// Set the systems default logger
+// Default logs will be captured and handled by revel at level info
+func SetDefaultLog(fromLog MultiLogger) {
+ log.SetOutput(loggerRewrite{Logger: fromLog, Level: log15.LvlInfo, hideDeprecated: true})
+ // No need to show date and time, that will be logged with revel
+ log.SetFlags(0)
+}
+
+func (rl *RevelLogger) Debugf(msg string, param ...interface{}) {
+ rl.Debug(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted info message
+func (rl *RevelLogger) Infof(msg string, param ...interface{}) {
+ rl.Info(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted warn message
+func (rl *RevelLogger) Warnf(msg string, param ...interface{}) {
+ rl.Warn(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted error message
+func (rl *RevelLogger) Errorf(msg string, param ...interface{}) {
+ rl.Error(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted critical message
+func (rl *RevelLogger) Critf(msg string, param ...interface{}) {
+ rl.Crit(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted fatal message
+func (rl *RevelLogger) Fatalf(msg string, param ...interface{}) {
+ rl.Fatal(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted panic message
+func (rl *RevelLogger) Panicf(msg string, param ...interface{}) {
+ rl.Panic(fmt.Sprintf(msg, param...))
+}
+
+// Print a critical message and call os.Exit(1)
+func (rl *RevelLogger) Fatal(msg string, ctx ...interface{}) {
+ rl.Crit(msg, ctx...)
+ os.Exit(1)
+}
+
+// Print a critical message and panic
+func (rl *RevelLogger) Panic(msg string, ctx ...interface{}) {
+ rl.Crit(msg, ctx...)
+ panic(msg)
+}
+
+// Override log15 method
+func (rl *RevelLogger) New(ctx ...interface{}) MultiLogger {
+ old := &RevelLogger{Logger: rl.Logger.New(ctx...)}
+ return old
+}
+
+// Set the stack level to check for the caller
+func (rl *RevelLogger) SetStackDepth(amount int) MultiLogger {
+ rl.Logger.SetStackDepth(amount) // Ignore the logger returned
+ return rl
+}
+
+// Create a new logger
+func New(ctx ...interface{}) MultiLogger {
+ r := &RevelLogger{Logger: log15.New(ctx...)}
+ r.SetStackDepth(1)
+ return r
+}
+
+// Set the handler in the Logger
+func (rl *RevelLogger) SetHandler(h LogHandler) {
+ rl.Logger.SetHandler(callHandler(h.Log))
+}
+
+// The function wrapper to implement the callback
+type callHandler func(r *Record) error
+
+// Log implementation, reads the record and extracts the details from the log record
+// Hiding the implementation.
+func (c callHandler) Log(log *log15.Record) error {
+ ctx := log.Ctx
+ var ctxMap ContextMap
+ if len(ctx) > 0 {
+ ctxMap = make(ContextMap, len(ctx)/2)
+
+ for i := 0; i < len(ctx); i += 2 {
+ v := ctx[i]
+ key, ok := v.(string)
+ if !ok {
+ key = fmt.Sprintf("LOGGER_INVALID_KEY %v", v)
+ }
+ var value interface{}
+ if len(ctx) > i+1 {
+ value = ctx[i+1]
+ } else {
+ value = "LOGGER_VALUE_MISSING"
+ }
+ ctxMap[key] = value
+ }
+ } else {
+ ctxMap = make(ContextMap, 0)
+ }
+ r := &Record{Message: log.Msg, Context: ctxMap, Time: log.Time, Level: LogLevel(log.Lvl), Call: CallStack(log.Call)}
+ return c(r)
+}
+
+// Internally used contextMap, allows conversion of map to map[string]string
+type ContextMap map[string]interface{}
+
+// Convert the context map to be string only values, any non string values are ignored
+func (m ContextMap) StringMap() (newMap map[string]string) {
+ if m != nil {
+ newMap = map[string]string{}
+ for key, value := range m {
+ if svalue, isstring := value.(string); isstring {
+ newMap[key] = svalue
+ }
+ }
+ }
+ return
+}
+func (m ContextMap) Add(key string, value interface{}) {
+ m[key] = value
+}
--- /dev/null
+package logger
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strconv"
+ "sync"
+ "time"
+)
+
+const (
+ timeFormat = "2006-01-02T15:04:05-0700"
+ termTimeFormat = "2006/01/02 15:04:05"
+ termSmallTimeFormat = "15:04:05"
+ floatFormat = 'f'
+ errorKey = "REVEL_ERROR"
+)
+
+var (
+ levelString = map[LogLevel]string{LvlDebug: "DEBUG",
+ LvlInfo: "INFO", LvlWarn: "WARN", LvlError: "ERROR", LvlCrit: "CRIT"}
+)
+
+// Outputs to the terminal in a format like below
+// INFO 09:11:32 server-engine.go:169: Request Stats
+func TerminalFormatHandler(noColor bool, smallDate bool) LogFormat {
+ dateFormat := termTimeFormat
+ if smallDate {
+ dateFormat = termSmallTimeFormat
+ }
+ return FormatFunc(func(r *Record) []byte {
+ // Bash coloring http://misc.flogisoft.com/bash/tip_colors_and_formatting
+ var color = 0
+ switch r.Level {
+ case LvlCrit:
+ // Magenta
+ color = 35
+ case LvlError:
+ // Red
+ color = 31
+ case LvlWarn:
+ // Yellow
+ color = 33
+ case LvlInfo:
+ // Green
+ color = 32
+ case LvlDebug:
+ // Cyan
+ color = 36
+ }
+
+ b := &bytes.Buffer{}
+ caller, _ := r.Context["caller"].(string)
+ module, _ := r.Context["module"].(string)
+ if noColor == false && color > 0 {
+ if len(module) > 0 {
+ fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %6s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
+ } else {
+ fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), caller, r.Message)
+ }
+ } else {
+ fmt.Fprintf(b, "%-5s %s %6s %13s: %-40s", levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
+ }
+
+ i := 0
+ for k, v := range r.Context {
+ if i != 0 {
+ b.WriteByte(' ')
+ }
+ i++
+ if k == "module" || k == "caller" {
+ continue
+ }
+
+ v := formatLogfmtValue(v)
+
+ // TODO: we should probably check that all of your key bytes aren't invalid
+ if noColor == false && color > 0 {
+ fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m=%s", color, k, v)
+ } else {
+ b.WriteString(k)
+ b.WriteByte('=')
+ b.WriteString(v)
+ }
+ }
+
+ b.WriteByte('\n')
+
+ return b.Bytes()
+ })
+}
+
+// formatValue formats a value for serialization
+func formatLogfmtValue(value interface{}) string {
+ if value == nil {
+ return "nil"
+ }
+
+ if t, ok := value.(time.Time); ok {
+ // Performance optimization: No need for escaping since the provided
+ // timeFormat doesn't have any escape characters, and escaping is
+ // expensive.
+ return t.Format(termTimeFormat)
+ }
+ value = formatShared(value)
+ switch v := value.(type) {
+ case bool:
+ return strconv.FormatBool(v)
+ case float32:
+ return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
+ case float64:
+ return strconv.FormatFloat(v, floatFormat, 7, 64)
+ case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+ return fmt.Sprintf("%d", value)
+ case string:
+ return escapeString(v)
+ default:
+ return escapeString(fmt.Sprintf("%+v", value))
+ }
+}
+
+// Format the value in json format
+func formatShared(value interface{}) (result interface{}) {
+ defer func() {
+ if err := recover(); err != nil {
+ if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
+ result = "nil"
+ } else {
+ panic(err)
+ }
+ }
+ }()
+
+ switch v := value.(type) {
+ case time.Time:
+ return v.Format(timeFormat)
+
+ case error:
+ return v.Error()
+
+ case fmt.Stringer:
+ return v.String()
+
+ default:
+ return v
+ }
+}
+
+// A reusuable buffer for outputting data
+var stringBufPool = sync.Pool{
+ New: func() interface{} { return new(bytes.Buffer) },
+}
+
+// Escape the string when needed
+func escapeString(s string) string {
+ needsQuotes := false
+ needsEscape := false
+ for _, r := range s {
+ if r <= ' ' || r == '=' || r == '"' {
+ needsQuotes = true
+ }
+ if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' {
+ needsEscape = true
+ }
+ }
+ if needsEscape == false && needsQuotes == false {
+ return s
+ }
+ e := stringBufPool.Get().(*bytes.Buffer)
+ e.WriteByte('"')
+ for _, r := range s {
+ switch r {
+ case '\\', '"':
+ e.WriteByte('\\')
+ e.WriteByte(byte(r))
+ case '\n':
+ e.WriteString("\\n")
+ case '\r':
+ e.WriteString("\\r")
+ case '\t':
+ e.WriteString("\\t")
+ default:
+ e.WriteRune(r)
+ }
+ }
+ e.WriteByte('"')
+ var ret string
+ if needsQuotes {
+ ret = e.String()
+ } else {
+ ret = string(e.Bytes()[1 : e.Len()-1])
+ }
+ e.Reset()
+ stringBufPool.Put(e)
+ return ret
+}
+
+// JsonFormatEx formats log records as JSON objects. If pretty is true,
+// records will be pretty-printed. If lineSeparated is true, records
+// will be logged with a new line between each record.
+func JsonFormatEx(pretty, lineSeparated bool) LogFormat {
+ jsonMarshal := json.Marshal
+ if pretty {
+ jsonMarshal = func(v interface{}) ([]byte, error) {
+ return json.MarshalIndent(v, "", " ")
+ }
+ }
+
+ return FormatFunc(func(r *Record) []byte {
+ props := make(map[string]interface{})
+
+ props["t"] = r.Time
+ props["lvl"] = levelString[r.Level]
+ props["msg"] = r.Message
+ for k, v := range r.Context {
+ props[k] = formatJsonValue(v)
+ }
+
+ b, err := jsonMarshal(props)
+ if err != nil {
+ b, _ = jsonMarshal(map[string]string{
+ errorKey: err.Error(),
+ })
+ return b
+ }
+
+ if lineSeparated {
+ b = append(b, '\n')
+ }
+
+ return b
+ })
+}
+
+func formatJsonValue(value interface{}) interface{} {
+ value = formatShared(value)
+ switch value.(type) {
+ case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
+ return value
+ default:
+ return fmt.Sprintf("%+v", value)
+ }
+}
--- /dev/null
+package logger
+
+import (
+ "github.com/revel/log15"
+ "gopkg.in/stack.v0"
+ "log"
+)
+
+// Utility package to make existing logging backwards compatible
+var (
+ // Convert the string to LogLevel
+ toLevel = map[string]LogLevel{"debug": LogLevel(log15.LvlDebug),
+ "info": LogLevel(log15.LvlInfo), "request": LogLevel(log15.LvlInfo), "warn": LogLevel(log15.LvlWarn),
+ "error": LogLevel(log15.LvlError), "crit": LogLevel(log15.LvlCrit),
+ "trace": LogLevel(log15.LvlDebug), // TODO trace is deprecated, replaced by debug
+ }
+)
+
+const (
+ // The test mode flag overrides the default log level and shows only errors
+ TEST_MODE_FLAG = "testModeFlag"
+ // The special use flag enables showing messages when the logger is setup
+ SPECIAL_USE_FLAG = "specialUseFlag"
+)
+
+// Returns the logger for the name
+func GetLogger(name string, logger MultiLogger) (l *log.Logger) {
+ switch name {
+ case "trace": // TODO trace is deprecated, replaced by debug
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
+ case "debug":
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
+ case "info":
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
+ case "warn":
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlWarn}, "", 0)
+ case "error":
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlError}, "", 0)
+ case "request":
+ l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
+ }
+
+ return l
+
+}
+
+// Used by the initFilterLog to handle the filters
+var logFilterList = []struct {
+ LogPrefix, LogSuffix string
+ parentHandler func(map[string]interface{}) ParentLogHandler
+}{{
+ "log.", ".filter",
+ func(keyMap map[string]interface{}) ParentLogHandler {
+ return NewParentLogHandler(func(child LogHandler) LogHandler {
+ return MatchMapHandler(keyMap, child)
+ })
+
+ },
+}, {
+ "log.", ".nfilter",
+ func(keyMap map[string]interface{}) ParentLogHandler {
+ return NewParentLogHandler(func(child LogHandler) LogHandler {
+ return NotMatchMapHandler(keyMap, child)
+ })
+ },
+}}
+
+// This structure and method will handle the old output format and log it to the new format
+type loggerRewrite struct {
+ Logger MultiLogger
+ Level log15.Lvl
+ hideDeprecated bool
+}
+
+// The message indicating that a logger is using a deprecated log mechanism
+var log_deprecated = []byte("* LOG DEPRECATED * ")
+
+// Implements the Write of the logger
+func (lr loggerRewrite) Write(p []byte) (n int, err error) {
+ if !lr.hideDeprecated {
+ p = append(log_deprecated, p...)
+ }
+ n = len(p)
+ if len(p) > 0 && p[n-1] == '\n' {
+ p = p[:n-1]
+ n--
+ }
+
+ switch lr.Level {
+ case log15.LvlInfo:
+ lr.Logger.Info(string(p))
+ case log15.LvlDebug:
+ lr.Logger.Debug(string(p))
+ case log15.LvlWarn:
+ lr.Logger.Warn(string(p))
+ case log15.LvlError:
+ lr.Logger.Error(string(p))
+ case log15.LvlCrit:
+ lr.Logger.Crit(string(p))
+ }
+
+ return
+}
+
+// For logging purposes the call stack can be used to record the stack trace of a bad error
+// simply pass it as a context field in your log statement like
+// `controller.Log.Crit("This should not occur","stack",revel.NewCallStack())`
+func NewCallStack() interface{} {
+ return stack.Trace()
+}
--- /dev/null
+package logger
+
+// FuncHandler returns a Handler that logs records with the given
+// function.
+import (
+ "fmt"
+ "reflect"
+ "sync"
+ "time"
+)
+
+// Function handler wraps the declared function and returns the handler for it
+func FuncHandler(fn func(r *Record) error) LogHandler {
+ return funcHandler(fn)
+}
+
+// The type decleration for the function
+type funcHandler func(r *Record) error
+
+// The implementation of the Log
+func (h funcHandler) Log(r *Record) error {
+ return h(r)
+}
+
+// This function allows you to do a full declaration for the log,
+// it is recommended you use FuncHandler instead
+func HandlerFunc(log func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error) LogHandler {
+ return remoteHandler(log)
+}
+
+// The type used for the HandlerFunc
+type remoteHandler func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error
+
+// The Log implementation
+func (c remoteHandler) Log(record *Record) error {
+ return c(record.Message, record.Time, record.Level, record.Call, record.Context)
+}
+
+// SyncHandler can be wrapped around a handler to guarantee that
+// only a single Log operation can proceed at a time. It's necessary
+// for thread-safe concurrent writes.
+func SyncHandler(h LogHandler) LogHandler {
+ var mu sync.Mutex
+ return FuncHandler(func(r *Record) error {
+ defer mu.Unlock()
+ mu.Lock()
+ return h.Log(r)
+ })
+}
+
+// LazyHandler writes all values to the wrapped handler after evaluating
+// any lazy functions in the record's context. It is already wrapped
+// around StreamHandler and SyslogHandler in this library, you'll only need
+// it if you write your own Handler.
+func LazyHandler(h LogHandler) LogHandler {
+ return FuncHandler(func(r *Record) error {
+ for k, v := range r.Context {
+ if lz, ok := v.(Lazy); ok {
+ value, err := evaluateLazy(lz)
+ if err != nil {
+ r.Context[errorKey] = "bad lazy " + k
+ } else {
+ v = value
+ }
+ }
+ }
+
+ return h.Log(r)
+ })
+}
+
+func evaluateLazy(lz Lazy) (interface{}, error) {
+ t := reflect.TypeOf(lz.Fn)
+
+ if t.Kind() != reflect.Func {
+ return nil, fmt.Errorf("INVALID_LAZY, not func: %+v", lz.Fn)
+ }
+
+ if t.NumIn() > 0 {
+ return nil, fmt.Errorf("INVALID_LAZY, func takes args: %+v", lz.Fn)
+ }
+
+ if t.NumOut() == 0 {
+ return nil, fmt.Errorf("INVALID_LAZY, no func return val: %+v", lz.Fn)
+ }
+
+ value := reflect.ValueOf(lz.Fn)
+ results := value.Call([]reflect.Value{})
+ if len(results) == 1 {
+ return results[0].Interface(), nil
+ } else {
+ values := make([]interface{}, len(results))
+ for i, v := range results {
+ values[i] = v.Interface()
+ }
+ return values, nil
+ }
+}
--- /dev/null
+package model
+
+import "github.com/revel/revel/utils"
+
+// The single instance object that has the config populated to it
+type RevelContainer struct {
+ Controller struct {
+ Reuse bool // True if the controllers are reused Set via revel.controller.reuse
+ Stack *utils.SimpleLockStack // size set by revel.controller.stack, revel.controller.maxstack
+ CachedMap map[string]*utils.SimpleLockStack // The map of reusable controllers
+ CachedStackSize int // The default size of each stack in CachedMap Set via revel.cache.controller.stack
+ CachedStackMaxSize int // The max size of each stack in CachedMap Set via revel.cache.controller.maxstack
+ }
+}
--- /dev/null
+package revel
+
+import (
+ "fmt"
+ "github.com/revel/revel/logger"
+ "go/build"
+ "gopkg.in/stack.v0"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+// Module specific functions
+type Module struct {
+ Name, ImportPath, Path string
+ ControllerTypeList []*ControllerType
+ Log logger.MultiLogger
+ initializedModules map[string]ModuleCallbackInterface
+}
+
+// Modules can be called back after they are loaded in revel by using this interface.
+type ModuleCallbackInterface func(*Module)
+
+// The namespace separator constant
+const namespaceSeperator = `\` // (note cannot be . or : as this is already used for routes)
+
+var (
+ Modules []*Module // The list of modules in use
+ anyModule = &Module{} // Wildcard search for controllers for a module (for backward compatible lookups)
+ appModule = &Module{Name: "App", initializedModules: map[string]ModuleCallbackInterface{}, Log: AppLog} // The app module
+ moduleLog = RevelLog.New("section", "module")
+)
+
+// Called by a module init() function, caller will receive the *Module object created for that module
+// This would be useful for assigning a logger for logging information in the module (since the module context would be correct)
+func RegisterModuleInit(callback ModuleCallbackInterface) {
+ // Store the module that called this so we can do a callback when the app is initialized
+ // The format %+k is from go-stack/Call.Format and returns the package path
+ key := fmt.Sprintf("%+k", stack.Caller(1))
+ appModule.initializedModules[key] = callback
+ if Initialized {
+ RevelLog.Error("Application already initialized, initializing using app module", "key", key)
+ callback(appModule)
+ }
+}
+
+// Called on startup to make a callback so that modules can be initialized through the `RegisterModuleInit` function
+func init() {
+ AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+ if typeOf == REVEL_BEFORE_MODULES_LOADED {
+ Modules = []*Module{appModule}
+ appModule.Path = filepath.ToSlash(AppPath)
+ appModule.ImportPath = filepath.ToSlash(AppPath)
+ }
+
+ return
+ })
+}
+
+// Returns the namespace for the module in the format `module_name|`
+func (m *Module) Namespace() (namespace string) {
+ namespace = m.Name + namespaceSeperator
+ return
+}
+
+// Returns the named controller and action that is in this module
+func (m *Module) ControllerByName(name, action string) (ctype *ControllerType) {
+ comparison := name
+ if strings.Index(name, namespaceSeperator) < 0 {
+ comparison = m.Namespace() + name
+ }
+ for _, c := range m.ControllerTypeList {
+ if c.Name() == comparison {
+ ctype = c
+ break
+ }
+ }
+ return
+}
+
+// Adds the controller type to this module
+func (m *Module) AddController(ct *ControllerType) {
+ m.ControllerTypeList = append(m.ControllerTypeList, ct)
+}
+
+// Based on the full path given return the relevant module
+// Only to be used on initialization
+func ModuleFromPath(path string, addGopathToPath bool) (module *Module) {
+ path = filepath.ToSlash(path)
+ gopathList := filepath.SplitList(build.Default.GOPATH)
+ // Strip away the vendor folder
+ if i := strings.Index(path, "/vendor/"); i > 0 {
+ path = path[i+len("vendor/"):]
+ }
+
+ // See if the path exists in the module based
+ for i := range Modules {
+ if addGopathToPath {
+ for _, gopath := range gopathList {
+ if strings.Contains(filepath.ToSlash(filepath.Clean(filepath.Join(gopath, "src", path))), Modules[i].Path) {
+ module = Modules[i]
+ break
+ }
+ }
+ } else {
+ if strings.Contains(path, Modules[i].ImportPath) {
+ module = Modules[i]
+ break
+ }
+
+ }
+
+ if module != nil {
+ break
+ }
+ }
+ // Default to the app module if not found
+ if module == nil {
+ module = appModule
+ }
+ return
+}
+
+// ModuleByName returns the module of the given name, if loaded, case insensitive.
+func ModuleByName(name string) (*Module, bool) {
+ // If the name ends with the namespace separator remove it
+ if name[len(name)-1] == []byte(namespaceSeperator)[0] {
+ name = name[:len(name)-1]
+ }
+ name = strings.ToLower(name)
+ if name == strings.ToLower(appModule.Name) {
+ return appModule, true
+ }
+ for _, module := range Modules {
+ if strings.ToLower(module.Name) == name {
+ return module, true
+ }
+ }
+ return nil, false
+}
+
+// Loads the modules specified in the config
+func loadModules() {
+ keys := []string{}
+ for _, key := range Config.Options("module.") {
+ keys = append(keys, key)
+ }
+
+ // Reorder module order by key name, a poor mans sort but at least it is consistent
+ sort.Strings(keys)
+ for _, key := range keys {
+ moduleLog.Debug("Sorted keys", "keys", key)
+
+ }
+ for _, key := range keys {
+ moduleImportPath := Config.StringDefault(key, "")
+ if moduleImportPath == "" {
+ continue
+ }
+
+ modulePath, err := ResolveImportPath(moduleImportPath)
+ if err != nil {
+ moduleLog.Error("Failed to load module. Import of path failed", "modulePath", moduleImportPath, "error", err)
+ }
+ // Drop anything between module.???.<name of module>
+ subKey := key[len("module."):]
+ if index := strings.Index(subKey, "."); index > -1 {
+ subKey = subKey[index+1:]
+ }
+ addModule(subKey, moduleImportPath, modulePath)
+ }
+
+ // Modules loaded, now show module path
+ for key, callback := range appModule.initializedModules {
+ if m := ModuleFromPath(key, false); m != nil {
+ callback(m)
+ } else {
+ RevelLog.Error("Callback for non registered module initializing with application module", "modulePath", key)
+ callback(appModule)
+ }
+ }
+}
+
+// called by `loadModules`, creates a new `Module` instance and appends it to the `Modules` list
+func addModule(name, importPath, modulePath string) {
+ if _, found := ModuleByName(name); found {
+ moduleLog.Panic("Attempt to import duplicate module %s path %s aborting startup", "name", name, "path", modulePath)
+ }
+ Modules = append(Modules, &Module{Name: name,
+ ImportPath: filepath.ToSlash(importPath),
+ Path: filepath.ToSlash(modulePath),
+ Log: RootLog.New("module", name)})
+ if codePath := filepath.Join(modulePath, "app"); DirExists(codePath) {
+ CodePaths = append(CodePaths, codePath)
+ if viewsPath := filepath.Join(modulePath, "app", "views"); DirExists(viewsPath) {
+ TemplatePaths = append(TemplatePaths, viewsPath)
+ }
+ }
+
+ moduleLog.Debug("Loaded module ", "module", filepath.Base(modulePath))
+
+ // Hack: There is presently no way for the testrunner module to add the
+ // "test" subdirectory to the CodePaths. So this does it instead.
+ if importPath == Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") {
+ joinedPath := filepath.Join(BasePath, "tests")
+ moduleLog.Debug("Found testrunner module, adding `tests` path ", "path", joinedPath)
+ CodePaths = append(CodePaths, joinedPath)
+ }
+ if testsPath := filepath.Join(modulePath, "tests"); DirExists(testsPath) {
+ moduleLog.Debug("Found tests path ", "path", testsPath)
+ CodePaths = append(CodePaths, testsPath)
+ }
+}
--- /dev/null
+package revel
+
+import (
+ "bytes"
+ "regexp"
+)
+
+// Module matching template syntax allows for modules to replace this text with the name of the module declared on import
+// this allows the reverse router to use correct syntax
+// Match _LOCAL_.static or _LOCAL_|
+var namespaceReplacement = regexp.MustCompile(`(_LOCAL_)(\.(.*?))?\\`)
+
+// Function to replace the bytes data that may match the _LOCAL_ namespace specifier,
+// the replacement will be the current module.Name
+func namespaceReplace(fileBytes []byte, module *Module) []byte {
+ newBytes, lastIndex := &bytes.Buffer{}, 0
+ matches := namespaceReplacement.FindAllSubmatchIndex(fileBytes, -1)
+ for _, match := range matches {
+ // Write up to first bytes
+ newBytes.Write(fileBytes[lastIndex:match[0]])
+ // skip ahead index to match[1]
+ lastIndex = match[3]
+ if match[4] > 0 {
+ // This match includes the module name as imported by the module
+ // We could transform the module name if it is different..
+ // For now leave it the same
+ // so _LOCAL_.static| becomes static|
+ lastIndex++
+ } else {
+ // Inject the module name
+ newBytes.Write([]byte(module.Name))
+ }
+ }
+ // Write remainder of document
+ newBytes.Write(fileBytes[lastIndex:])
+ return newBytes.Bytes()
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "net/http"
+ "runtime/debug"
+)
+
+// PanicFilter wraps the action invocation in a protective defer blanket that
+// converts panics into 500 error pages.
+func PanicFilter(c *Controller, fc []Filter) {
+ defer func() {
+ if err := recover(); err != nil {
+ handleInvocationPanic(c, err)
+ }
+ }()
+ fc[0](c, fc[1:])
+}
+
+// This function handles a panic in an action invocation.
+// It cleans up the stack trace, logs it, and displays an error page.
+func handleInvocationPanic(c *Controller, err interface{}) {
+ error := NewErrorFromPanic(err)
+ if error != nil {
+ utilLog.Error("PanicFilter: Caught panic", "error", err, "stack", error.Stack)
+ if DevMode {
+ fmt.Println(err)
+ fmt.Println(error.Stack)
+ }
+ } else {
+ utilLog.Error("PanicFilter: Caught panic, unable to determine stack location", "error", err, "stack", string(debug.Stack()))
+ if DevMode {
+ fmt.Println(err)
+ fmt.Println("stack", string(debug.Stack()))
+ }
+ }
+
+ if error == nil && DevMode {
+ // Only show the sensitive information in the debug stack trace in development mode, not production
+ c.Response.SetStatus(http.StatusInternalServerError)
+ _, _ = c.Response.GetWriter().Write(debug.Stack())
+ return
+ }
+
+ c.Result = c.RenderError(error)
+}
--- /dev/null
+// Copyright (c) 2012-2017 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "mime/multipart"
+ "net/url"
+ "os"
+ "reflect"
+)
+
+// Params provides a unified view of the request params.
+// Includes:
+// - URL query string
+// - Form values
+// - File uploads
+//
+// Warning: param maps other than Values may be nil if there were none.
+type Params struct {
+ url.Values // A unified view of all the individual param maps below.
+
+ // Set by the router
+ Fixed url.Values // Fixed parameters from the route, e.g. App.Action("fixed param")
+ Route url.Values // Parameters extracted from the route, e.g. /customers/{id}
+
+ // Set by the ParamsFilter
+ Query url.Values // Parameters from the query string, e.g. /index?limit=10
+ Form url.Values // Parameters from the request body.
+
+ Files map[string][]*multipart.FileHeader // Files uploaded in a multipart form
+ tmpFiles []*os.File // Temp files used during the request.
+ JSON []byte // JSON data from request body
+}
+
+var paramsLogger = RevelLog.New("section", "params")
+
+// ParseParams parses the `http.Request` params into `revel.Controller.Params`
+func ParseParams(params *Params, req *Request) {
+ params.Query = req.GetQuery()
+
+ // Parse the body depending on the content type.
+ switch req.ContentType {
+ case "application/x-www-form-urlencoded":
+ // Typical form.
+ var err error
+ if params.Form, err = req.GetForm(); err != nil {
+ paramsLogger.Warn("ParseParams: Error parsing request body", "error", err)
+ }
+
+ case "multipart/form-data":
+ // Multipart form.
+ if mp, err := req.GetMultipartForm(); err != nil {
+ paramsLogger.Warn("ParseParams: parsing request body:", "error", err)
+ } else {
+ params.Form = mp.GetValues()
+ params.Files = mp.GetFiles()
+ }
+ case "application/json":
+ fallthrough
+ case "text/json":
+ if body := req.GetBody(); body != nil {
+ if content, err := ioutil.ReadAll(body); err == nil {
+ // We wont bind it until we determine what we are binding too
+ params.JSON = content
+ } else {
+ paramsLogger.Error("ParseParams: Failed to ready request body bytes", "error", err)
+ }
+ } else {
+ paramsLogger.Info("ParseParams: Json post received with empty body")
+ }
+ }
+
+ params.Values = params.calcValues()
+}
+
+// Bind looks for the named parameter, converts it to the requested type, and
+// writes it into "dest", which must be settable. If the value can not be
+// parsed, "dest" is set to the zero value.
+func (p *Params) Bind(dest interface{}, name string) {
+ value := reflect.ValueOf(dest)
+ if value.Kind() != reflect.Ptr {
+ paramsLogger.Panic("Bind: revel/params: non-pointer passed to Bind: " + name)
+ }
+ value = value.Elem()
+ if !value.CanSet() {
+ paramsLogger.Panic("Bind: revel/params: non-settable variable passed to Bind: " + name)
+ }
+
+ // Remove the json from the Params, this will stop the binder from attempting
+ // to use the json data to populate the destination interface. We do not want
+ // to do this on a named bind directly against the param, it is ok to happen when
+ // the action is invoked.
+ jsonData := p.JSON
+ p.JSON = nil
+ value.Set(Bind(p, name, value.Type()))
+ p.JSON = jsonData
+}
+
+// Bind binds the JSON data to the dest.
+func (p *Params) BindJSON(dest interface{}) error {
+ value := reflect.ValueOf(dest)
+ if value.Kind() != reflect.Ptr {
+ paramsLogger.Warn("BindJSON: Not a pointer")
+ return errors.New("BindJSON not a pointer")
+ }
+ if err := json.Unmarshal(p.JSON, dest); err != nil {
+ paramsLogger.Warn("BindJSON: Unable to unmarshal request:", "error", err)
+ return err
+ }
+ return nil
+}
+
+// calcValues returns a unified view of the component param maps.
+func (p *Params) calcValues() url.Values {
+ numParams := len(p.Query) + len(p.Fixed) + len(p.Route) + len(p.Form)
+
+ // If there were no params, return an empty map.
+ if numParams == 0 {
+ return make(url.Values, 0)
+ }
+
+ // If only one of the param sources has anything, return that directly.
+ switch numParams {
+ case len(p.Query):
+ return p.Query
+ case len(p.Route):
+ return p.Route
+ case len(p.Fixed):
+ return p.Fixed
+ case len(p.Form):
+ return p.Form
+ }
+
+ // Copy everything into a param map,
+ // order of priority is least to most trusted
+ values := make(url.Values, numParams)
+
+ // ?query string parameters are first
+ for k, v := range p.Query {
+ values[k] = append(values[k], v...)
+ }
+
+ // form parameters append
+ for k, v := range p.Form {
+ values[k] = append(values[k], v...)
+ }
+
+ // :/path parameters overwrite
+ for k, v := range p.Route {
+ values[k] = v
+ }
+
+ // fixed route parameters overwrite
+ for k, v := range p.Fixed {
+ values[k] = v
+ }
+
+ return values
+}
+
+func ParamsFilter(c *Controller, fc []Filter) {
+ ParseParams(c.Params, c.Request)
+
+ // Clean up from the request.
+ defer func() {
+ for _, tmpFile := range c.Params.tmpFiles {
+ err := os.Remove(tmpFile.Name())
+ if err != nil {
+ paramsLogger.Warn("ParamsFilter: Could not remove upload temp file:", err)
+ }
+ }
+ }()
+
+ fc[0](c, fc[1:])
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+// Params: Testing Multipart forms
+
+const (
+ MultipartBoundary = "A"
+ MultipartFormData = `--A
+Content-Disposition: form-data; name="text1"
+
+data1
+--A
+Content-Disposition: form-data; name="text2"
+
+data2
+--A
+Content-Disposition: form-data; name="text2"
+
+data3
+--A
+Content-Disposition: form-data; name="file1"; filename="test.txt"
+Content-Type: text/plain
+
+content1
+--A
+Content-Disposition: form-data; name="file2[]"; filename="test.txt"
+Content-Type: text/plain
+
+content2
+--A
+Content-Disposition: form-data; name="file2[]"; filename="favicon.ico"
+Content-Type: image/x-icon
+
+xyz
+--A
+Content-Disposition: form-data; name="file3[0]"; filename="test.txt"
+Content-Type: text/plain
+
+content3
+--A
+Content-Disposition: form-data; name="file3[1]"; filename="favicon.ico"
+Content-Type: image/x-icon
+
+zzz
+--A--
+`
+)
+
+// The values represented by the form data.
+type fh struct {
+ filename string
+ content []byte
+}
+
+var (
+ expectedValues = map[string][]string{
+ "text1": {"data1"},
+ "text2": {"data2", "data3"},
+ }
+ expectedFiles = map[string][]fh{
+ "file1": {fh{"test.txt", []byte("content1")}},
+ "file2[]": {fh{"test.txt", []byte("content2")}, fh{"favicon.ico", []byte("xyz")}},
+ "file3[0]": {fh{"test.txt", []byte("content3")}},
+ "file3[1]": {fh{"favicon.ico", []byte("zzz")}},
+ }
+)
+
+func getMultipartRequest() *http.Request {
+ req, _ := http.NewRequest("POST", "http://localhost/path",
+ bytes.NewBufferString(MultipartFormData))
+ req.Header.Set(
+ "Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", MultipartBoundary))
+ req.Header.Set(
+ "Content-Length", fmt.Sprintf("%d", len(MultipartFormData)))
+ return req
+}
+
+func BenchmarkParams(b *testing.B) {
+ c := NewTestController(nil, showRequest)
+ c.Params = &Params{}
+
+ for i := 0; i < b.N; i++ {
+ ParamsFilter(c, NilChain)
+ }
+}
+
+func TestMultipartForm(t *testing.T) {
+ c := NewTestController(nil, getMultipartRequest())
+ c.Params = &Params{}
+
+ ParamsFilter(c, NilChain)
+
+ if !reflect.DeepEqual(expectedValues, map[string][]string(c.Params.Values)) {
+ t.Errorf("Param values: (expected) %v != %v (actual)",
+ expectedValues, map[string][]string(c.Params.Values))
+ }
+
+ actualFiles := make(map[string][]fh)
+ for key, fileHeaders := range c.Params.Files {
+ for _, fileHeader := range fileHeaders {
+ file, _ := fileHeader.Open()
+ content, _ := ioutil.ReadAll(file)
+ actualFiles[key] = append(actualFiles[key], fh{fileHeader.Filename, content})
+ }
+ }
+
+ if !reflect.DeepEqual(expectedFiles, actualFiles) {
+ t.Errorf("Param files: (expected) %v != %v (actual)", expectedFiles, actualFiles)
+ }
+}
+
+func TestBind(t *testing.T) {
+ params := Params{
+ Values: url.Values{
+ "x": {"5"},
+ },
+ }
+ var x int
+ params.Bind(&x, "x")
+ if x != 5 {
+ t.Errorf("Failed to bind x. Value: %d", x)
+ }
+}
+
+func TestResolveAcceptLanguage(t *testing.T) {
+ request := buildHTTPRequestWithAcceptLanguage("")
+ if result := ResolveAcceptLanguage(request); result != nil {
+ t.Errorf("Expected Accept-Language to resolve to an empty string but it was '%s'", result)
+ }
+
+ request = buildHTTPRequestWithAcceptLanguage("en-GB,en;q=0.8,nl;q=0.6")
+ if result := ResolveAcceptLanguage(request); len(result) != 3 {
+ t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3)
+ } else {
+ if result[0].Language != "en-GB" {
+ t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-GB", result[0].Language)
+ }
+ if result[1].Language != "en" {
+ t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en", result[1].Language)
+ }
+ if result[2].Language != "nl" {
+ t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "nl", result[2].Language)
+ }
+ }
+
+ request = buildHTTPRequestWithAcceptLanguage("en;q=0.8,nl;q=0.6,en-AU;q=malformed")
+ if result := ResolveAcceptLanguage(request); len(result) != 3 {
+ t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3)
+ } else {
+ if result[0].Language != "en-AU" {
+ t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-AU", result[0].Language)
+ }
+ }
+}
+
+func BenchmarkResolveAcceptLanguage(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ 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")
+ ResolveAcceptLanguage(request)
+ }
+}
+
+func buildHTTPRequestWithAcceptLanguage(acceptLanguage string) *Request {
+ request, _ := http.NewRequest("POST", "http://localhost/path", nil)
+ request.Header.Set("Accept-Language", acceptLanguage)
+ c := NewTestController(nil, request)
+
+ return c.Request
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type Result interface {
+ Apply(req *Request, resp *Response)
+}
+
+// ErrorResult structure used to handles all kinds of error codes (500, 404, ..).
+// It renders the relevant error page (errors/CODE.format, e.g. errors/500.json).
+// If RunMode is "dev", this results in a friendly error page.
+type ErrorResult struct {
+ ViewArgs map[string]interface{}
+ Error error
+}
+
+var resultsLog = RevelLog.New("section", "results")
+
+func (r ErrorResult) Apply(req *Request, resp *Response) {
+ format := req.Format
+ status := resp.Status
+ if status == 0 {
+ status = http.StatusInternalServerError
+ }
+
+ contentType := ContentTypeByFilename("xxx." + format)
+ if contentType == DefaultFileContentType {
+ contentType = "text/plain"
+ }
+ lang, _ := r.ViewArgs[CurrentLocaleViewArg].(string)
+ // Get the error template.
+ var err error
+ templatePath := fmt.Sprintf("errors/%d.%s", status, format)
+ tmpl, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+
+ // This func shows a plaintext error message, in case the template rendering
+ // doesn't work.
+ showPlaintext := func(err error) {
+ PlaintextErrorResult{fmt.Errorf("Server Error:\n%s\n\n"+
+ "Additionally, an error occurred when rendering the error page:\n%s",
+ r.Error, err)}.Apply(req, resp)
+ }
+
+ if tmpl == nil {
+ if err == nil {
+ err = fmt.Errorf("Couldn't find template %s", templatePath)
+ }
+ templateLog.Warn("Got an error rendering template", "error", err, "template", templatePath, "lang", lang)
+ showPlaintext(err)
+ return
+ }
+
+ // If it's not a revel error, wrap it in one.
+ var revelError *Error
+ switch e := r.Error.(type) {
+ case *Error:
+ revelError = e
+ case error:
+ revelError = &Error{
+ Title: "Server Error",
+ Description: e.Error(),
+ }
+ }
+
+ if revelError == nil {
+ panic("no error provided")
+ }
+
+ if r.ViewArgs == nil {
+ r.ViewArgs = make(map[string]interface{})
+ }
+ r.ViewArgs["RunMode"] = RunMode
+ r.ViewArgs["DevMode"] = DevMode
+ r.ViewArgs["Error"] = revelError
+ r.ViewArgs["Router"] = MainRouter
+
+ resultsLog.Info("Rendering error template", "template", templatePath, "error", revelError)
+
+ // Render it.
+ var b bytes.Buffer
+ err = tmpl.Render(&b, r.ViewArgs)
+
+ // If there was an error, print it in plain text.
+ if err != nil {
+ templateLog.Warn("Got an error rendering template", "error", err, "template", templatePath, "lang", lang)
+ showPlaintext(err)
+ return
+ }
+
+ // need to check if we are on a websocket here
+ // net/http panics if we write to a hijacked connection
+ if req.Method == "WS" {
+ if err := req.WebSocket.MessageSendJSON(fmt.Sprint(revelError)); err != nil {
+ resultsLog.Error("Apply: Send failed", "error", err)
+ }
+ } else {
+ resp.WriteHeader(status, contentType)
+ if _, err := b.WriteTo(resp.GetWriter()); err != nil {
+ resultsLog.Error("Apply: Response WriteTo failed:", "error", err)
+ }
+ }
+
+}
+
+type PlaintextErrorResult struct {
+ Error error
+}
+
+// Apply method is used when the template loader or error template is not available.
+func (r PlaintextErrorResult) Apply(req *Request, resp *Response) {
+ resp.WriteHeader(http.StatusInternalServerError, "text/plain; charset=utf-8")
+ if _, err := resp.GetWriter().Write([]byte(r.Error.Error())); err != nil {
+ resultsLog.Error("Apply: Write error:", "error", err)
+ }
+}
+
+// RenderTemplateResult action methods returns this result to request
+// a template be rendered.
+type RenderTemplateResult struct {
+ Template Template
+ ViewArgs map[string]interface{}
+}
+
+func (r *RenderTemplateResult) Apply(req *Request, resp *Response) {
+ // Handle panics when rendering templates.
+ defer func() {
+ if err := recover(); err != nil {
+ resultsLog.Error("Apply: panic recovery", "error", err)
+ PlaintextErrorResult{fmt.Errorf("Template Execution Panic in %s:\n%s",
+ r.Template.Name(), err)}.Apply(req, resp)
+ }
+ }()
+
+ chunked := Config.BoolDefault("results.chunked", false)
+
+ // If it's a HEAD request, throw away the bytes.
+ out := io.Writer(resp.GetWriter())
+ if req.Method == "HEAD" {
+ out = ioutil.Discard
+ }
+
+ // In a prod mode, write the status, render, and hope for the best.
+ // (In a dev mode, always render to a temporary buffer first to avoid having
+ // error pages distorted by HTML already written)
+ if chunked && !DevMode {
+ resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+ if err := r.renderOutput(out); err != nil {
+ r.renderError(err, req, resp)
+ }
+ return
+ }
+
+ // Render the template into a temporary buffer, to see if there was an error
+ // rendering the template. If not, then copy it into the response buffer.
+ // Otherwise, template render errors may result in unpredictable HTML (and
+ // would carry a 200 status code)
+ b, err := r.ToBytes()
+ if err != nil {
+ r.renderError(err, req, resp)
+ return
+ }
+
+ if !chunked {
+ resp.Out.Header().Set("Content-Length", strconv.Itoa(b.Len()))
+ }
+ resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+ if _, err := b.WriteTo(out); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+}
+
+// Return a byte array and or an error object if the template failed to render
+func (r *RenderTemplateResult) ToBytes() (b *bytes.Buffer, err error) {
+ defer func() {
+ if rerr := recover(); rerr != nil {
+ resultsLog.Error("ApplyBytes: panic recovery", "recover-error", rerr)
+ err = fmt.Errorf("Template Execution Panic in %s:\n%s", r.Template.Name(), rerr)
+ }
+ }()
+ b = &bytes.Buffer{}
+ if err = r.renderOutput(b); err == nil {
+ if Config.BoolDefault("results.trim.html", false) {
+ b = r.compressHtml(b)
+ }
+ }
+ return
+}
+
+// Output the template to the writer, catch any panics and return as an error
+func (r *RenderTemplateResult) renderOutput(wr io.Writer) (err error) {
+ defer func() {
+ if rerr := recover(); rerr != nil {
+ resultsLog.Error("ApplyBytes: panic recovery", "recover-error", rerr)
+ err = fmt.Errorf("Template Execution Panic in %s:\n%s", r.Template.Name(), rerr)
+ }
+ }()
+ err = r.Template.Render(wr, r.ViewArgs)
+ return
+}
+
+// Trimming the HTML will do the following:
+// * Remove all leading & trailing whitespace on every line
+// * Remove all empty lines
+// * Attempt to keep formatting inside <pre></pre> tags
+//
+// This is safe unless white-space: pre; is used in css for formatting.
+// Since there is no way to detect that, you will have to keep trimming off in these cases.
+func (r *RenderTemplateResult) compressHtml(b *bytes.Buffer) (b2 *bytes.Buffer) {
+
+ // Allocate length of original buffer, so we can write everything without allocating again
+ b2.Grow(b.Len())
+ insidePre := false
+ for {
+ text, err := b.ReadString('\n')
+ // Convert to lower case for finding <pre> tags.
+ tl := strings.ToLower(text)
+ if strings.Contains(tl, "<pre>") {
+ insidePre = true
+ }
+ // Trim if not inside a <pre> statement
+ if !insidePre {
+ // Cut trailing/leading whitespace
+ text = strings.Trim(text, " \t\r\n")
+ if len(text) > 0 {
+ if _, err = b2.WriteString(text); err != nil {
+ resultsLog.Error("Apply: ", "error", err)
+ }
+ if _, err = b2.WriteString("\n"); err != nil {
+ resultsLog.Error("Apply: ", "error", err)
+ }
+ }
+ } else {
+ if _, err = b2.WriteString(text); err != nil {
+ resultsLog.Error("Apply: ", "error", err)
+ }
+ }
+ if strings.Contains(tl, "</pre>") {
+ insidePre = false
+ }
+ // We are finished
+ if err != nil {
+ break
+ }
+ }
+
+ return
+}
+
+// Render the error in the response
+func (r *RenderTemplateResult) renderError(err error, req *Request, resp *Response) {
+ compileError, found := err.(*Error)
+ if !found {
+ var templateContent []string
+ templateName, line, description := ParseTemplateError(err)
+ if templateName == "" {
+ templateLog.Info("Cannot determine template name to render error", "error", err)
+ templateName = r.Template.Name()
+ templateContent = r.Template.Content()
+
+ } else {
+ lang, _ := r.ViewArgs[CurrentLocaleViewArg].(string)
+ if tmpl, err := MainTemplateLoader.TemplateLang(templateName, lang); err == nil {
+ templateContent = tmpl.Content()
+ } else {
+ templateLog.Info("Unable to retreive template ", "error", err)
+ }
+ }
+ compileError = &Error{
+ Title: "Template Execution Error",
+ Path: templateName,
+ Description: description,
+ Line: line,
+ SourceLines: templateContent,
+ }
+ }
+ resp.Status = 500
+ resultsLog.Errorf("render: Template Execution Error (in %s): %s", compileError.Path, compileError.Description)
+ ErrorResult{r.ViewArgs, compileError}.Apply(req, resp)
+}
+
+type RenderHTMLResult struct {
+ html string
+}
+
+func (r RenderHTMLResult) Apply(req *Request, resp *Response) {
+ resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+ if _, err := resp.GetWriter().Write([]byte(r.html)); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+}
+
+type RenderJSONResult struct {
+ obj interface{}
+ callback string
+}
+
+func (r RenderJSONResult) Apply(req *Request, resp *Response) {
+ var b []byte
+ var err error
+ if Config.BoolDefault("results.pretty", false) {
+ b, err = json.MarshalIndent(r.obj, "", " ")
+ } else {
+ b, err = json.Marshal(r.obj)
+ }
+
+ if err != nil {
+ ErrorResult{Error: err}.Apply(req, resp)
+ return
+ }
+
+ if r.callback == "" {
+ resp.WriteHeader(http.StatusOK, "application/json; charset=utf-8")
+ if _, err = resp.GetWriter().Write(b); err != nil {
+ resultsLog.Error("Apply: Response write failed:", "error", err)
+ }
+ return
+ }
+
+ resp.WriteHeader(http.StatusOK, "application/javascript; charset=utf-8")
+ if _, err = resp.GetWriter().Write([]byte(r.callback + "(")); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+ if _, err = resp.GetWriter().Write(b); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+ if _, err = resp.GetWriter().Write([]byte(");")); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+}
+
+type RenderXMLResult struct {
+ obj interface{}
+}
+
+func (r RenderXMLResult) Apply(req *Request, resp *Response) {
+ var b []byte
+ var err error
+ if Config.BoolDefault("results.pretty", false) {
+ b, err = xml.MarshalIndent(r.obj, "", " ")
+ } else {
+ b, err = xml.Marshal(r.obj)
+ }
+
+ if err != nil {
+ ErrorResult{Error: err}.Apply(req, resp)
+ return
+ }
+
+ resp.WriteHeader(http.StatusOK, "application/xml; charset=utf-8")
+ if _, err = resp.GetWriter().Write(b); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+}
+
+type RenderTextResult struct {
+ text string
+}
+
+func (r RenderTextResult) Apply(req *Request, resp *Response) {
+ resp.WriteHeader(http.StatusOK, "text/plain; charset=utf-8")
+ if _, err := resp.GetWriter().Write([]byte(r.text)); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+}
+
+type ContentDisposition string
+
+var (
+ NoDisposition ContentDisposition = ""
+ Attachment ContentDisposition = "attachment"
+ Inline ContentDisposition = "inline"
+)
+
+type BinaryResult struct {
+ Reader io.Reader
+ Name string
+ Length int64
+ Delivery ContentDisposition
+ ModTime time.Time
+}
+
+func (r *BinaryResult) Apply(req *Request, resp *Response) {
+ if r.Delivery != NoDisposition {
+ disposition := string(r.Delivery)
+ if r.Name != "" {
+ disposition += fmt.Sprintf(`; filename="%s"`, r.Name)
+ }
+ resp.Out.internalHeader.Set("Content-Disposition", disposition)
+ }
+ if resp.ContentType != "" {
+ resp.Out.internalHeader.Set("Content-Type", resp.ContentType)
+ } else {
+ contentType := ContentTypeByFilename(r.Name)
+ resp.Out.internalHeader.Set("Content-Type", contentType)
+ }
+ if content, ok := r.Reader.(io.ReadSeeker); ok && r.Length < 0 {
+ // get the size from the stream
+ // go1.6 compatibility change, go1.6 does not define constants io.SeekStart
+ //if size, err := content.Seek(0, io.SeekEnd); err == nil {
+ // if _, err = content.Seek(0, io.SeekStart); err == nil {
+ if size, err := content.Seek(0, 2); err == nil {
+ if _, err = content.Seek(0, 0); err == nil {
+ r.Length = size
+ }
+ }
+ }
+
+ // Write stream writes the status code to the header as well
+ if ws := resp.GetStreamWriter(); ws != nil {
+ if err := ws.WriteStream(r.Name, r.Length, r.ModTime, r.Reader); err != nil {
+ resultsLog.Error("Apply: Response write failed", "error", err)
+ }
+ }
+
+ // Close the Reader if we can
+ if v, ok := r.Reader.(io.Closer); ok {
+ _ = v.Close()
+ }
+}
+
+type RedirectToURLResult struct {
+ url string
+}
+
+func (r *RedirectToURLResult) Apply(req *Request, resp *Response) {
+ resp.Out.internalHeader.Set("Location", r.url)
+ resp.WriteHeader(http.StatusFound, "")
+}
+
+type RedirectToActionResult struct {
+ val interface{}
+ args []interface{}
+}
+
+func (r *RedirectToActionResult) Apply(req *Request, resp *Response) {
+ url, err := getRedirectURL(r.val, r.args)
+ if err != nil {
+ resultsLog.Error("Apply: Couldn't resolve redirect", "error", err)
+ ErrorResult{Error: err}.Apply(req, resp)
+ return
+ }
+ resp.Out.internalHeader.Set("Location", url)
+ resp.WriteHeader(http.StatusFound, "")
+}
+
+func getRedirectURL(item interface{}, args []interface{}) (string, error) {
+ // Handle strings
+ if url, ok := item.(string); ok {
+ return url, nil
+ }
+
+ // Handle funcs
+ val := reflect.ValueOf(item)
+ typ := reflect.TypeOf(item)
+ if typ.Kind() == reflect.Func && typ.NumIn() > 0 {
+ // Get the Controller Method
+ recvType := typ.In(0)
+ method := FindMethod(recvType, val)
+ if method == nil {
+ return "", errors.New("couldn't find method")
+ }
+
+ // Construct the action string (e.g. "Controller.Method")
+ if recvType.Kind() == reflect.Ptr {
+ recvType = recvType.Elem()
+ }
+ module := ModuleFromPath(recvType.PkgPath(), true)
+ action := module.Namespace() + recvType.Name() + "." + method.Name
+ // Fetch the action path to get the defaults
+ pathData, found := splitActionPath(nil, action, true)
+ if !found {
+ return "", fmt.Errorf("Unable to redirect '%s', expected 'Controller.Action'", action)
+ }
+
+ // Build the map for the router to reverse
+ // Unbind the arguments.
+ argsByName := make(map[string]string)
+ // Bind any static args first
+ fixedParams := len(pathData.FixedParamsByName)
+ methodType := pathData.TypeOfController.Method(pathData.MethodName)
+
+ for i, argValue := range args {
+ Unbind(argsByName, methodType.Args[i+fixedParams].Name, argValue)
+ }
+
+ actionDef := MainRouter.Reverse(action, argsByName)
+ if actionDef == nil {
+ return "", errors.New("no route for action " + action)
+ }
+
+ return actionDef.String(), nil
+ }
+
+ // Out of guesses
+ return "", errors.New("didn't recognize type: " + typ.String())
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// Added test case for redirection testing for strings
+func TestRedirect(t *testing.T) {
+ startFakeBookingApp()
+ redirect := RedirectToURLResult{fmt.Sprintf("/hotels/index/foo")}
+ resp := httptest.NewRecorder()
+ c := NewTestController(resp, showRequest)
+ redirect.Apply(c.Request, c.Response)
+ if resp.Header().Get("Location") != "/hotels/index/foo" {
+ t.Errorf("Failed to set redirect header correctly. : %s", resp.Header().Get("Location"))
+ }
+}
+
+// Test that the render response is as expected.
+func TestBenchmarkRender(t *testing.T) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ t.Errorf("SetAction failed: %s", err)
+ }
+ result := Hotels{c}.Show(3)
+ result.Apply(c.Request, c.Response)
+ if !strings.Contains(resp.Body.String(), "300 Main St.") {
+ t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+ }
+}
+
+func BenchmarkRenderChunked(b *testing.B) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ resp.Body = nil
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ b.Errorf("SetAction failed: %s", err)
+ }
+ Config.SetOption("results.chunked", "true")
+ b.ResetTimer()
+
+ hotels := Hotels{c}
+ for i := 0; i < b.N; i++ {
+ hotels.Show(3).Apply(c.Request, c.Response)
+ }
+}
+
+func BenchmarkRenderNotChunked(b *testing.B) {
+ startFakeBookingApp()
+ resp := httptest.NewRecorder()
+ resp.Body = nil
+ c := NewTestController(resp, showRequest)
+ if err := c.SetAction("Hotels", "Show"); err != nil {
+ b.Errorf("SetAction failed: %s", err)
+ }
+ Config.SetOption("results.chunked", "false")
+ b.ResetTimer()
+
+ hotels := Hotels{c}
+ for i := 0; i < b.N; i++ {
+ hotels.Show(3).Apply(c.Request, c.Response)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "go/build"
+ "log"
+ "path/filepath"
+ "strings"
+
+ "encoding/json"
+ "fmt"
+ "github.com/revel/config"
+ "github.com/revel/revel/logger"
+ "github.com/revel/revel/model"
+)
+
+const (
+ // RevelImportPath Revel framework import path
+ RevelImportPath = "github.com/revel/revel"
+)
+
+const (
+ TEST_MODE_FLAG = "testModeFlag"
+ SPECIAL_USE_FLAG = "specialUseFlag"
+)
+
+// App details
+var (
+ RevelConfig *model.RevelContainer
+ AppName string // e.g. "sample"
+ AppRoot string // e.g. "/app1"
+ BasePath string // e.g. "$GOPATH/src/corp/sample"
+ AppPath string // e.g. "$GOPATH/src/corp/sample/app"
+ ViewsPath string // e.g. "$GOPATH/src/corp/sample/app/views"
+ ImportPath string // e.g. "corp/sample"
+ SourcePath string // e.g. "$GOPATH/src"
+
+ Config *config.Context
+ RunMode string // Application-defined (by default, "dev" or "prod")
+ DevMode bool // if true, RunMode is a development mode.
+
+ // Revel installation details
+ RevelPath string // e.g. "$GOPATH/src/github.com/revel/revel"
+
+ // Where to look for templates
+ // Ordered by priority. (Earlier paths take precedence over later paths.)
+ CodePaths []string // Code base directories, for modules and app
+ TemplatePaths []string // Template path directories manually added
+
+ // ConfPaths where to look for configurations
+ // Config load order
+ // 1. framework (revel/conf/*)
+ // 2. application (conf/*)
+ // 3. user supplied configs (...) - User configs can override/add any from above
+ ConfPaths []string
+
+ // Server config.
+ //
+ // Alert: This is how the app is configured, which may be different from
+ // the current process reality. For example, if the app is configured for
+ // port 9000, HTTPPort will always be 9000, even though in dev mode it is
+ // run on a random port and proxied.
+ HTTPPort int // e.g. 9000
+ HTTPAddr string // e.g. "", "127.0.0.1"
+ HTTPSsl bool // e.g. true if using ssl
+ HTTPSslCert string // e.g. "/path/to/cert.pem"
+ HTTPSslKey string // e.g. "/path/to/key.pem"
+
+ // All cookies dropped by the framework begin with this prefix.
+ CookiePrefix string
+ // Cookie domain
+ CookieDomain string
+ // Cookie flags
+ CookieSecure bool
+
+ // Revel request access log, not exposed from package.
+ // However output settings can be controlled from app.conf
+
+ // True when revel engine has been initialized (Init has returned)
+ Initialized bool
+
+ // Private
+ secretKey []byte // Key used to sign cookies. An empty key disables signing.
+ packaged bool // If true, this is running from a pre-built package.
+ initEventList = []EventHandler{} // Event handler list for receiving events
+)
+
+// Init initializes Revel -- it provides paths for getting around the app.
+//
+// Params:
+// mode - the run mode, which determines which app.conf settings are used.
+// importPath - the Go import path of the application.
+// srcPath - the path to the source directory, containing Revel and the app.
+// If not specified (""), then a functioning Go installation is required.
+func Init(inputmode, importPath, srcPath string) {
+ RevelConfig = &model.RevelContainer{}
+ // Ignore trailing slashes.
+ ImportPath = strings.TrimRight(importPath, "/")
+ SourcePath = srcPath
+
+ RunMode = updateLog(inputmode)
+
+ // If the SourcePath is not specified, find it using build.Import.
+ var revelSourcePath string // may be different from the app source path
+ if SourcePath == "" {
+ revelSourcePath, SourcePath = findSrcPaths(importPath)
+ } else {
+ // If the SourcePath was specified, assume both Revel and the app are within it.
+ SourcePath = filepath.Clean(SourcePath)
+ revelSourcePath = SourcePath
+ packaged = true
+ }
+
+ RevelPath = filepath.Join(revelSourcePath, filepath.FromSlash(RevelImportPath))
+ BasePath = filepath.Join(SourcePath, filepath.FromSlash(importPath))
+ AppPath = filepath.Join(BasePath, "app")
+ ViewsPath = filepath.Join(AppPath, "views")
+
+ CodePaths = []string{AppPath}
+
+ if ConfPaths == nil {
+ ConfPaths = []string{}
+ }
+
+ // Config load order
+ // 1. framework (revel/conf/*)
+ // 2. application (conf/*)
+ // 3. user supplied configs (...) - User configs can override/add any from above
+ ConfPaths = append(
+ []string{
+ filepath.Join(RevelPath, "conf"),
+ filepath.Join(BasePath, "conf"),
+ },
+ ConfPaths...)
+
+ TemplatePaths = []string{
+ ViewsPath,
+ filepath.Join(RevelPath, "templates"),
+ }
+
+ // Load app.conf
+ var err error
+ Config, err = config.LoadContext("app.conf", ConfPaths)
+ if err != nil || Config == nil {
+ RevelLog.Fatal("Failed to load app.conf:", "error", err)
+ }
+
+ // After application config is loaded update the logger
+ updateLog(inputmode)
+
+ // Configure properties from app.conf
+ DevMode = Config.BoolDefault("mode.dev", false)
+ HTTPPort = Config.IntDefault("http.port", 9000)
+ HTTPAddr = Config.StringDefault("http.addr", "")
+ HTTPSsl = Config.BoolDefault("http.ssl", false)
+ HTTPSslCert = Config.StringDefault("http.sslcert", "")
+ HTTPSslKey = Config.StringDefault("http.sslkey", "")
+ if HTTPSsl {
+ if HTTPSslCert == "" {
+ RevelLog.Fatal("No http.sslcert provided.")
+ }
+ if HTTPSslKey == "" {
+ RevelLog.Fatal("No http.sslkey provided.")
+ }
+ }
+
+ AppName = Config.StringDefault("app.name", "(not set)")
+ AppRoot = Config.StringDefault("app.root", "")
+ CookiePrefix = Config.StringDefault("cookie.prefix", "REVEL")
+ CookieDomain = Config.StringDefault("cookie.domain", "")
+ CookieSecure = Config.BoolDefault("cookie.secure", HTTPSsl)
+ if secretStr := Config.StringDefault("app.secret", ""); secretStr != "" {
+ SetSecretKey([]byte(secretStr))
+ }
+
+ RaiseEvent(REVEL_BEFORE_MODULES_LOADED, nil)
+ loadModules()
+ RaiseEvent(REVEL_AFTER_MODULES_LOADED, nil)
+
+ Initialized = true
+ RevelLog.Info("Initialized Revel", "Version", Version, "BuildDate", BuildDate, "MinimumGoVersion", MinimumGoVersion)
+}
+
+// The input mode can be as simple as "prod" or it can be a JSON string like
+// {"mode":"%s","testModeFlag":true}
+// When this function is called it returns the true "inputmode" extracted from the parameter
+// and it sets the log context appropriately
+func updateLog(inputmode string) (returnMode string) {
+ if inputmode == "" {
+ returnMode = config.DefaultSection
+ return
+ } else {
+ returnMode = inputmode
+ }
+
+ // Check to see if the mode is a json object
+ modemap := map[string]interface{}{}
+
+ var testModeFlag, specialUseFlag bool
+ if err := json.Unmarshal([]byte(inputmode), &modemap); err == nil {
+ returnMode = modemap["mode"].(string)
+ if testmode, found := modemap[TEST_MODE_FLAG]; found {
+ testModeFlag, _ = testmode.(bool)
+ }
+ if specialUse, found := modemap[SPECIAL_USE_FLAG]; found {
+ specialUseFlag, _ = specialUse.(bool)
+ }
+ }
+
+ var newContext *config.Context
+ // If the Config is nil, set the logger to minimal log messages by adding the option
+ if Config == nil {
+ newContext = config.NewContext()
+ newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(true))
+ } else {
+ // Ensure that the selected runmode appears in app.conf.
+ // If empty string is passed as the mode, treat it as "DEFAULT"
+ if !Config.HasSection(returnMode) {
+ log.Fatalln("app.conf: No mode found:", returnMode)
+ }
+ Config.SetSection(returnMode)
+ newContext = Config
+ }
+
+ // Only set the testmode flag if it doesnt exist
+ if _, found := newContext.Bool(TEST_MODE_FLAG); !found {
+ newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(testModeFlag))
+ }
+ if _, found := newContext.Bool(SPECIAL_USE_FLAG); !found {
+ newContext.SetOption(SPECIAL_USE_FLAG, fmt.Sprint(specialUseFlag))
+ }
+
+ appHandle := logger.InitializeFromConfig(BasePath, newContext)
+
+ // Set all the log handlers
+ setAppLog(AppLog, appHandle)
+
+ return
+}
+
+// Set the secret key
+func SetSecretKey(newKey []byte) error {
+ secretKey = newKey
+ return nil
+}
+
+// ResolveImportPath returns the filesystem path for the given import path.
+// Returns an error if the import path could not be found.
+func ResolveImportPath(importPath string) (string, error) {
+ if packaged {
+ return filepath.Join(SourcePath, importPath), nil
+ }
+
+ modPkg, err := build.Import(importPath, RevelPath, build.FindOnly)
+ if err != nil {
+ return "", err
+ }
+ return modPkg.Dir, nil
+}
+
+// CheckInit method checks `revel.Initialized` if not initialized it panics
+func CheckInit() {
+ if !Initialized {
+ RevelLog.Panic("CheckInit: Revel has not been initialized!")
+ }
+}
+
+// findSrcPaths uses the "go/build" package to find the source root for Revel
+// and the app.
+func findSrcPaths(importPath string) (revelSourcePath, appSourcePath string) {
+ var (
+ gopaths = filepath.SplitList(build.Default.GOPATH)
+ goroot = build.Default.GOROOT
+ )
+
+ if len(gopaths) == 0 {
+ RevelLog.Fatal("GOPATH environment variable is not set. " +
+ "Please refer to http://golang.org/doc/code.html to configure your Go environment.")
+ }
+
+ if ContainsString(gopaths, goroot) {
+ RevelLog.Fatalf("GOPATH (%s) must not include your GOROOT (%s). "+
+ "Please refer to http://golang.org/doc/code.html to configure your Go environment.",
+ gopaths, goroot)
+ }
+
+ appPkg, err := build.Import(importPath, "", build.FindOnly)
+ if err != nil {
+ RevelLog.Panic("Failed to import "+importPath+" with error:", "error", err)
+ }
+
+ revelPkg, err := build.Import(RevelImportPath, appPkg.Dir, build.FindOnly)
+ if err != nil {
+ RevelLog.Fatal("Failed to find Revel with error:", "error", err)
+ }
+
+ return revelPkg.Dir[:len(revelPkg.Dir)-len(RevelImportPath)], appPkg.SrcRoot
+}
--- /dev/null
+package revel
+
+import (
+ "sort"
+)
+
+// The list of startup hooks
+type (
+ // The startup hooks structure
+ RevelHook struct {
+ order int // The order
+ f func() // The function to call
+ }
+
+ RevelHooks []RevelHook
+)
+
+var (
+ // The local instance of the list
+ startupHooks RevelHooks
+
+ // The instance of the list
+ shutdownHooks RevelHooks
+)
+
+// Called to run the hooks
+func (r RevelHooks) Run() {
+ serverLogger.Infof("There is %d hooks need to run ...", len(r))
+ sort.Sort(r)
+ for i, hook := range r {
+ utilLog.Infof("Run the %d hook ...", i+1)
+ hook.f()
+ }
+}
+
+// Adds a new function hook, using the order
+func (r RevelHooks) Add(fn func(), order ...int) RevelHooks {
+ o := 1
+ if len(order) > 0 {
+ o = order[0]
+ }
+ return append(r, RevelHook{order: o, f: fn})
+}
+
+// Sorting function
+func (slice RevelHooks) Len() int {
+ return len(slice)
+}
+
+// Sorting function
+func (slice RevelHooks) Less(i, j int) bool {
+ return slice[i].order < slice[j].order
+}
+
+// Sorting function
+func (slice RevelHooks) Swap(i, j int) {
+ slice[i], slice[j] = slice[j], slice[i]
+}
+
+// OnAppStart registers a function to be run at app startup.
+//
+// The order you register the functions will be the order they are run.
+// You can think of it as a FIFO queue.
+// This process will happen after the config file is read
+// and before the server is listening for connections.
+//
+// Ideally, your application should have only one call to init() in the file init.go.
+// The reason being that the call order of multiple init() functions in
+// the same package is undefined.
+// Inside of init() call revel.OnAppStart() for each function you wish to register.
+//
+// Example:
+//
+// // from: yourapp/app/controllers/somefile.go
+// func InitDB() {
+// // do DB connection stuff here
+// }
+//
+// func FillCache() {
+// // fill a cache from DB
+// // this depends on InitDB having been run
+// }
+//
+// // from: yourapp/app/init.go
+// func init() {
+// // set up filters...
+//
+// // register startup functions
+// revel.OnAppStart(InitDB)
+// revel.OnAppStart(FillCache)
+// }
+//
+// This can be useful when you need to establish connections to databases or third-party services,
+// setup app components, compile assets, or any thing you need to do between starting Revel and accepting connections.
+//
+func OnAppStart(f func(), order ...int) {
+ startupHooks = startupHooks.Add(f, order...)
+}
+
+// Called to add a new stop hook
+func OnAppStop(f func(), order ...int) {
+ shutdownHooks = shutdownHooks.Add(f, order...)
+}
--- /dev/null
+package revel
+
+import (
+ "net/http"
+)
+
+func NewTestController(w http.ResponseWriter, r *http.Request) *Controller {
+ context := NewGoContext(nil)
+ context.Request.SetRequest(r)
+ context.Response.SetResponse(w)
+ c := NewController(context)
+ return c
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "encoding/csv"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "os"
+ "sync"
+
+ "github.com/revel/pathtree"
+ "github.com/revel/revel/logger"
+)
+
+const (
+ httpStatusCode = "404"
+)
+
+type Route struct {
+ ModuleSource *Module // Module name of route
+ Method string // e.g. GET
+ Path string // e.g. /app/:id
+ Action string // e.g. "Application.ShowApp", "404"
+ ControllerNamespace string // e.g. "testmodule.",
+ ControllerName string // e.g. "Application", ""
+ MethodName string // e.g. "ShowApp", ""
+ FixedParams []string // e.g. "arg1","arg2","arg3" (CSV formatting)
+ TreePath string // e.g. "/GET/app/:id"
+ TypeOfController *ControllerType // The controller type (if route is not wild carded)
+
+ routesPath string // e.g. /Users/robfig/gocode/src/myapp/conf/routes
+ line int // e.g. 3
+}
+
+type RouteMatch struct {
+ Action string // e.g. 404
+ ControllerName string // e.g. Application
+ MethodName string // e.g. ShowApp
+ FixedParams []string
+ Params map[string][]string // e.g. {id: 123}
+ TypeOfController *ControllerType // The controller type
+ ModuleSource *Module // The module
+}
+
+type ActionPathData struct {
+ Key string // The unique key
+ ControllerNamespace string // The controller namespace
+ ControllerName string // The controller name
+ MethodName string // The method name
+ Action string // The action
+ ModuleSource *Module // The module
+ Route *Route // The route
+ FixedParamsByName map[string]string // The fixed parameters
+ TypeOfController *ControllerType // The controller type
+}
+
+var (
+ // Used to store decoded action path mappings
+ actionPathCacheMap = map[string]*ActionPathData{}
+ // Used to prevent concurrent writes to map
+ actionPathCacheLock = sync.Mutex{}
+ // The path returned if not found
+ notFound = &RouteMatch{Action: "404"}
+)
+
+var routerLog = RevelLog.New("section", "router")
+
+func init() {
+ AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+ // Add in an
+ if typeOf == ROUTE_REFRESH_REQUESTED {
+ // Clear the actionPathCacheMap cache
+ actionPathCacheLock.Lock()
+ defer actionPathCacheLock.Unlock()
+ actionPathCacheMap = map[string]*ActionPathData{}
+ }
+ return
+ })
+}
+
+// NewRoute prepares the route to be used in matching.
+func NewRoute(moduleSource *Module, method, path, action, fixedArgs, routesPath string, line int) (r *Route) {
+ // Handle fixed arguments
+ argsReader := strings.NewReader(string(namespaceReplace([]byte(fixedArgs), moduleSource)))
+ csvReader := csv.NewReader(argsReader)
+ csvReader.TrimLeadingSpace = true
+ fargs, err := csvReader.Read()
+ if err != nil && err != io.EOF {
+ routerLog.Error("NewRoute: Invalid fixed parameters for string ", "error", err, "fixedargs", fixedArgs)
+ }
+
+ r = &Route{
+ ModuleSource: moduleSource,
+ Method: strings.ToUpper(method),
+ Path: path,
+ Action: string(namespaceReplace([]byte(action), moduleSource)),
+ FixedParams: fargs,
+ TreePath: treePath(strings.ToUpper(method), path),
+ routesPath: routesPath,
+ line: line,
+ }
+
+ // URL pattern
+ if !strings.HasPrefix(r.Path, "/") {
+ routerLog.Error("NewRoute: Absolute URL required.")
+ return
+ }
+
+ // Ignore the not found status code
+ if action != httpStatusCode {
+ routerLog.Debugf("NewRoute: New splitActionPath path:%s action:%s", path, action)
+ pathData, found := splitActionPath(&ActionPathData{ModuleSource: moduleSource, Route: r}, r.Action, false)
+ if found {
+ if pathData.TypeOfController != nil {
+ // Assign controller type to avoid looking it up based on name
+ r.TypeOfController = pathData.TypeOfController
+ // Create the fixed parameters
+ if l := len(pathData.Route.FixedParams); l > 0 && len(pathData.FixedParamsByName) == 0 {
+ methodType := pathData.TypeOfController.Method(pathData.MethodName)
+ if methodType != nil {
+ pathData.FixedParamsByName = make(map[string]string, l)
+ for i, argValue := range pathData.Route.FixedParams {
+ Unbind(pathData.FixedParamsByName, methodType.Args[i].Name, argValue)
+ }
+ } else {
+ routerLog.Panicf("NewRoute: Method %s not found for controller %s", pathData.MethodName, pathData.ControllerName)
+ }
+ }
+ }
+ r.ControllerNamespace = pathData.ControllerNamespace
+ r.ControllerName = pathData.ControllerName
+ r.ModuleSource = pathData.ModuleSource
+ r.MethodName = pathData.MethodName
+
+ // The same action path could be used for multiple routes (like the Static.Serve)
+ } else {
+ routerLog.Panicf("NewRoute: Failed to find controller for route path action %s \n%#v\n", path+"?"+r.Action, actionPathCacheMap)
+ }
+ }
+ return
+}
+
+func (route *Route) ActionPath() string {
+ return route.ModuleSource.Namespace() + route.ControllerName
+}
+
+func treePath(method, path string) string {
+ if method == "*" {
+ method = ":METHOD"
+ }
+ return "/" + method + path
+}
+
+type Router struct {
+ Routes []*Route
+ Tree *pathtree.Node
+ Module string // The module the route is associated with
+ path string // path to the routes file
+}
+
+func (router *Router) Route(req *Request) (routeMatch *RouteMatch) {
+ // Override method if set in header
+ if method := req.GetHttpHeader("X-HTTP-Method-Override"); method != "" && req.Method == "POST" {
+ req.Method = method
+ }
+
+ leaf, expansions := router.Tree.Find(treePath(req.Method, req.GetPath()))
+ if leaf == nil {
+ return nil
+ }
+
+ // Create a map of the route parameters.
+ var params url.Values
+ if len(expansions) > 0 {
+ params = make(url.Values)
+ for i, v := range expansions {
+ params[leaf.Wildcards[i]] = []string{v}
+ }
+ }
+ var route *Route
+ var controllerName, methodName string
+
+ // The leaf value is now a list of possible routes to match, only a controller
+ routeList := leaf.Value.([]*Route)
+ var typeOfController *ControllerType
+
+ //INFO.Printf("Found route for path %s %#v", req.URL.Path, len(routeList))
+ for index := range routeList {
+ route = routeList[index]
+ methodName = route.MethodName
+
+ // Special handling for explicit 404's.
+ if route.Action == httpStatusCode {
+ route = nil
+ break
+ }
+
+ // If wildcard match on method name use the method name from the params
+ if methodName[0] == ':' {
+ if methodKey, found := params[methodName[1:]]; found && len(methodKey) > 0 {
+ methodName = strings.ToLower(methodKey[0])
+ } else {
+ routerLog.Fatal("Route: Failure to find method name in parameters", "params", params, "methodName", methodName)
+ }
+ }
+
+ // If the action is variablized, replace into it with the captured args.
+ controllerName = route.ControllerName
+ if controllerName[0] == ':' {
+ controllerName = strings.ToLower(params[controllerName[1:]][0])
+ if typeOfController = route.ModuleSource.ControllerByName(controllerName, methodName); typeOfController != nil {
+ break
+ }
+ } else {
+ typeOfController = route.TypeOfController
+ break
+ }
+ route = nil
+ }
+
+ if route == nil {
+ routeMatch = notFound
+ } else {
+
+ routeMatch = &RouteMatch{
+ ControllerName: route.ControllerNamespace + controllerName,
+ MethodName: methodName,
+ Params: params,
+ FixedParams: route.FixedParams,
+ TypeOfController: typeOfController,
+ ModuleSource: route.ModuleSource,
+ }
+ }
+
+ return
+}
+
+// Refresh re-reads the routes file and re-calculates the routing table.
+// Returns an error if a specified action could not be found.
+func (router *Router) Refresh() (err *Error) {
+ RaiseEvent(ROUTE_REFRESH_REQUESTED, nil)
+ router.Routes, err = parseRoutesFile(appModule, router.path, "", true)
+ RaiseEvent(ROUTE_REFRESH_COMPLETED, nil)
+ if err != nil {
+ return
+ }
+ err = router.updateTree()
+ return
+}
+
+func (router *Router) updateTree() *Error {
+ router.Tree = pathtree.New()
+ pathMap := map[string][]*Route{}
+
+ allPathsOrdered := []string{}
+ // It is possible for some route paths to overlap
+ // based on wildcard matches,
+ // TODO when pathtree is fixed (made to be smart enough to not require a predefined intake order) keeping the routes in order is not necessary
+ for _, route := range router.Routes {
+ if _, found := pathMap[route.TreePath]; !found {
+ pathMap[route.TreePath] = append(pathMap[route.TreePath], route)
+ allPathsOrdered = append(allPathsOrdered, route.TreePath)
+ } else {
+ pathMap[route.TreePath] = append(pathMap[route.TreePath], route)
+ }
+ }
+ for _, path := range allPathsOrdered {
+ routeList := pathMap[path]
+ err := router.Tree.Add(path, routeList)
+
+ // Allow GETs to respond to HEAD requests.
+ if err == nil && routeList[0].Method == "GET" {
+ err = router.Tree.Add(treePath("HEAD", routeList[0].Path), routeList)
+ }
+
+ // Error adding a route to the pathtree.
+ if err != nil {
+ return routeError(err, path, fmt.Sprintf("%#v", routeList), routeList[0].line)
+ }
+ }
+ return nil
+}
+
+// Returns the controller namespace and name, action and module if found from the actionPath specified
+func splitActionPath(actionPathData *ActionPathData, actionPath string, useCache bool) (pathData *ActionPathData, found bool) {
+ actionPath = strings.ToLower(actionPath)
+ if pathData, found = actionPathCacheMap[actionPath]; found && useCache {
+ return
+ }
+ var (
+ controllerNamespace, controllerName, methodName, action string
+ foundModuleSource *Module
+ typeOfController *ControllerType
+ log = routerLog.New("actionPath", actionPath)
+ )
+ actionSplit := strings.Split(actionPath, ".")
+ if actionPathData != nil {
+ foundModuleSource = actionPathData.ModuleSource
+ }
+ if len(actionSplit) == 2 {
+ controllerName, methodName = strings.ToLower(actionSplit[0]), strings.ToLower(actionSplit[1])
+ if i := strings.Index(methodName, "("); i > 0 {
+ methodName = methodName[:i]
+ }
+ log = log.New("controller", controllerName, "method", methodName)
+ log.Debug("splitActionPath: Check for namespace")
+ if i := strings.Index(controllerName, namespaceSeperator); i > -1 {
+ controllerNamespace = controllerName[:i+1]
+ if moduleSource, found := ModuleByName(controllerNamespace[:len(controllerNamespace)-1]); found {
+ log.Debug("Found module namespace")
+ foundModuleSource = moduleSource
+ controllerNamespace = moduleSource.Namespace()
+ } else {
+ log.Warnf("splitActionPath: Unable to find module %s for action: %s", controllerNamespace[:len(controllerNamespace)-1], actionPath)
+ }
+ controllerName = controllerName[i+1:]
+ // Check for the type of controller
+ typeOfController = foundModuleSource.ControllerByName(controllerName, methodName)
+ found = typeOfController != nil
+ } else if controllerName[0] != ':' {
+ // First attempt to find the controller in the module source
+ if foundModuleSource != nil {
+ typeOfController = foundModuleSource.ControllerByName(controllerName, methodName)
+ if typeOfController != nil {
+ controllerNamespace = typeOfController.Namespace
+ }
+ }
+ log.Info("Found controller for path", "controllerType", typeOfController)
+
+ if typeOfController == nil {
+ // Check to see if we can determine the controller from only the controller name
+ // an actionPath without a moduleSource will only come from
+ // Scan through the controllers
+ matchName := controllerName
+ for key, controller := range controllers {
+ // Strip away the namespace from the controller. to be match
+ regularName := key
+ if i := strings.Index(key, namespaceSeperator); i > -1 {
+ regularName = regularName[i+1:]
+ }
+ if regularName == matchName {
+ // Found controller match
+ typeOfController = controller
+ controllerNamespace = typeOfController.Namespace
+ controllerName = typeOfController.ShortName()
+ foundModuleSource = typeOfController.ModuleSource
+ found = true
+ break
+ }
+ }
+ } else {
+ found = true
+ }
+ } else {
+ // If wildcard assign the route the controller namespace found
+ controllerNamespace = actionPathData.ModuleSource.Name + namespaceSeperator
+ foundModuleSource = actionPathData.ModuleSource
+ found = true
+ }
+ action = actionSplit[1]
+ } else {
+ foundPaths := ""
+ for path := range actionPathCacheMap {
+ foundPaths += path + ","
+ }
+ log.Warnf("splitActionPath: Invalid action path %s found paths %s", actionPath, foundPaths)
+ found = false
+ }
+
+ // Make sure no concurrent map writes occur
+ if found {
+ actionPathCacheLock.Lock()
+ defer actionPathCacheLock.Unlock()
+ if actionPathData != nil {
+ actionPathData.ControllerNamespace = controllerNamespace
+ actionPathData.ControllerName = controllerName
+ actionPathData.MethodName = methodName
+ actionPathData.Action = action
+ actionPathData.ModuleSource = foundModuleSource
+ actionPathData.TypeOfController = typeOfController
+ } else {
+ actionPathData = &ActionPathData{
+ ControllerNamespace: controllerNamespace,
+ ControllerName: controllerName,
+ MethodName: methodName,
+ Action: action,
+ ModuleSource: foundModuleSource,
+ TypeOfController: typeOfController,
+ }
+ }
+ actionPathData.TypeOfController = foundModuleSource.ControllerByName(controllerName, "")
+ if actionPathData.TypeOfController == nil && actionPathData.ControllerName[0] != ':' {
+ log.Warnf("splitActionPath: No controller found for %s %#v", foundModuleSource.Namespace()+controllerName, controllers)
+ }
+
+ pathData = actionPathData
+ if pathData.Route != nil && len(pathData.Route.FixedParams) > 0 {
+ // If there are fixed params on the route then add them to the path
+ // This will give it a unique path and it should still be usable for a reverse lookup provided the name is matchable
+ // for example
+ // GET /test/ Application.Index("Test", "Test2")
+ // {{url "Application.Index(test,test)" }}
+ // should be parseable
+ actionPath = actionPath + "(" + strings.ToLower(strings.Join(pathData.Route.FixedParams, ",")) + ")"
+ }
+ if actionPathData.Route != nil {
+ log.Debugf("splitActionPath: Split Storing recognized action path %s for route %#v ", actionPath, actionPathData.Route)
+ }
+ pathData.Key = actionPath
+ actionPathCacheMap[actionPath] = pathData
+ if !strings.Contains(actionPath, namespaceSeperator) && pathData.TypeOfController != nil {
+ actionPathCacheMap[strings.ToLower(pathData.TypeOfController.Namespace)+actionPath] = pathData
+ log.Debugf("splitActionPath: Split Storing recognized action path %s for route %#v ", strings.ToLower(pathData.TypeOfController.Namespace)+actionPath, actionPathData.Route)
+ }
+ }
+ return
+}
+
+// parseRoutesFile reads the given routes file and returns the contained routes.
+func parseRoutesFile(moduleSource *Module, routesPath, joinedPath string, validate bool) ([]*Route, *Error) {
+ contentBytes, err := ioutil.ReadFile(routesPath)
+ if err != nil {
+ return nil, &Error{
+ Title: "Failed to load routes file",
+ Description: err.Error(),
+ }
+ }
+ return parseRoutes(moduleSource, routesPath, joinedPath, string(contentBytes), validate)
+}
+
+// parseRoutes reads the content of a routes file into the routing table.
+func parseRoutes(moduleSource *Module, routesPath, joinedPath, content string, validate bool) ([]*Route, *Error) {
+ var routes []*Route
+
+ // For each line..
+ for n, line := range strings.Split(content, "\n") {
+ line = strings.TrimSpace(line)
+ if len(line) == 0 || line[0] == '#' {
+ continue
+ }
+
+ const modulePrefix = "module:"
+
+ // Handle included routes from modules.
+ // e.g. "module:testrunner" imports all routes from that module.
+ if strings.HasPrefix(line, modulePrefix) {
+ moduleRoutes, err := getModuleRoutes(line[len(modulePrefix):], joinedPath, validate)
+ if err != nil {
+ return nil, routeError(err, routesPath, content, n)
+ }
+ routes = append(routes, moduleRoutes...)
+ continue
+ }
+
+ // A single route
+ method, path, action, fixedArgs, found := parseRouteLine(line)
+ if !found {
+ continue
+ }
+
+ // this will avoid accidental double forward slashes in a route.
+ // this also avoids pathtree freaking out and causing a runtime panic
+ // because of the double slashes
+ if strings.HasSuffix(joinedPath, "/") && strings.HasPrefix(path, "/") {
+ joinedPath = joinedPath[0 : len(joinedPath)-1]
+ }
+ path = strings.Join([]string{AppRoot, joinedPath, path}, "")
+
+ // This will import the module routes under the path described in the
+ // routes file (joinedPath param). e.g. "* /jobs module:jobs" -> all
+ // routes' paths will have the path /jobs prepended to them.
+ // See #282 for more info
+ if method == "*" && strings.HasPrefix(action, modulePrefix) {
+ moduleRoutes, err := getModuleRoutes(action[len(modulePrefix):], path, validate)
+ if err != nil {
+ return nil, routeError(err, routesPath, content, n)
+ }
+ routes = append(routes, moduleRoutes...)
+ continue
+ }
+
+ route := NewRoute(moduleSource, method, path, action, fixedArgs, routesPath, n)
+ routes = append(routes, route)
+
+ if validate {
+ if err := validateRoute(route); err != nil {
+ return nil, routeError(err, routesPath, content, n)
+ }
+ }
+ }
+
+ return routes, nil
+}
+
+// validateRoute checks that every specified action exists.
+func validateRoute(route *Route) error {
+ // Skip 404s
+ if route.Action == httpStatusCode {
+ return nil
+ }
+
+ // Skip variable routes.
+ if route.ControllerName[0] == ':' || route.MethodName[0] == ':' {
+ return nil
+ }
+
+ // Precheck to see if controller exists
+ if _, found := controllers[route.ControllerNamespace+route.ControllerName]; !found {
+ // Scan through controllers to find module
+ for _, c := range controllers {
+ controllerName := strings.ToLower(c.Type.Name())
+ if controllerName == route.ControllerName {
+ route.ControllerNamespace = c.ModuleSource.Name + namespaceSeperator
+ routerLog.Warn("validateRoute: Matched empty namespace route for %s to this namespace %s for the route %s", controllerName, c.ModuleSource.Name, route.Path)
+ }
+ }
+ }
+
+ // TODO need to check later
+ // does it do only validation or validation and instantiate the controller.
+ var c Controller
+ return c.SetTypeAction(route.ControllerNamespace+route.ControllerName, route.MethodName, route.TypeOfController)
+}
+
+// routeError adds context to a simple error message.
+func routeError(err error, routesPath, content string, n int) *Error {
+ if revelError, ok := err.(*Error); ok {
+ return revelError
+ }
+ // Load the route file content if necessary
+ if content == "" {
+ if contentBytes, er := ioutil.ReadFile(routesPath); er != nil {
+ routerLog.Error("routeError: Failed to read route file ", "file", routesPath, "error", er)
+ } else {
+ content = string(contentBytes)
+ }
+ }
+ return &Error{
+ Title: "Route validation error",
+ Description: err.Error(),
+ Path: routesPath,
+ Line: n + 1,
+ SourceLines: strings.Split(content, "\n"),
+ Stack: fmt.Sprintf("%s", logger.NewCallStack()),
+ }
+}
+
+// getModuleRoutes loads the routes file for the given module and returns the
+// list of routes.
+func getModuleRoutes(moduleName, joinedPath string, validate bool) (routes []*Route, err *Error) {
+ // Look up the module. It may be not found due to the common case of e.g. the
+ // testrunner module being active only in dev mode.
+ module, found := ModuleByName(moduleName)
+ if !found {
+ routerLog.Debug("getModuleRoutes: Skipping routes for inactive module", "module", moduleName)
+ return nil, nil
+ }
+ routePath := filepath.Join(module.Path, "conf", "routes")
+ if _, e := os.Stat(routePath); e == nil {
+ routes, err = parseRoutesFile(module, routePath, joinedPath, validate)
+ }
+ if err == nil {
+ for _, route := range routes {
+ route.ModuleSource = module
+ }
+ }
+
+ return routes, err
+}
+
+// Groups:
+// 1: method
+// 4: path
+// 5: action
+// 6: fixedargs
+var routePattern = regexp.MustCompile(
+ "(?i)^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|WS|PROPFIND|MKCOL|COPY|MOVE|PROPPATCH|LOCK|UNLOCK|TRACE|PURGE|\\*)" +
+ "[(]?([^)]*)(\\))?[ \t]+" +
+ "(.*/[^ \t]*)[ \t]+([^ \t(]+)" +
+ `\(?([^)]*)\)?[ \t]*$`)
+
+func parseRouteLine(line string) (method, path, action, fixedArgs string, found bool) {
+ matches := routePattern.FindStringSubmatch(line)
+ if matches == nil {
+ return
+ }
+ method, path, action, fixedArgs = matches[1], matches[4], matches[5], matches[6]
+ found = true
+ return
+}
+
+func NewRouter(routesPath string) *Router {
+ return &Router{
+ Tree: pathtree.New(),
+ path: routesPath,
+ }
+}
+
+type ActionDefinition struct {
+ Host, Method, URL, Action string
+ Star bool
+ Args map[string]string
+}
+
+func (a *ActionDefinition) String() string {
+ return a.URL
+}
+
+func (router *Router) Reverse(action string, argValues map[string]string) (ad *ActionDefinition) {
+ log := routerLog.New("action", action)
+ pathData, found := splitActionPath(nil, action, true)
+ if !found {
+ routerLog.Error("splitActionPath: Failed to find reverse route", "action", action, "arguments", argValues)
+ return nil
+ }
+
+ log.Debug("Checking for route", "pathdataRoute", pathData.Route)
+ if pathData.Route == nil {
+ var possibleRoute *Route
+ // If the route is nil then we need to go through the routes to find the first matching route
+ // from this controllers namespace, this is likely a wildcard route match
+ for _, route := range router.Routes {
+ // Skip routes that are not wild card or empty
+ if route.ControllerName == "" || route.MethodName == "" {
+ continue
+ }
+ if route.ModuleSource == pathData.ModuleSource && route.ControllerName[0] == ':' {
+ // Wildcard match in same module space
+ pathData.Route = route
+ break
+ } else if route.ActionPath() == pathData.ModuleSource.Namespace()+pathData.ControllerName &&
+ (route.Method[0] == ':' || route.Method == pathData.MethodName) {
+ // Action path match
+ pathData.Route = route
+ break
+ } else if route.ControllerName == pathData.ControllerName &&
+ (route.Method[0] == ':' || route.Method == pathData.MethodName) {
+ // Controller name match
+ possibleRoute = route
+ }
+ }
+ if pathData.Route == nil && possibleRoute != nil {
+ pathData.Route = possibleRoute
+ routerLog.Warnf("Reverse: For a url reverse a match was based on %s matched path to route %#v ", action, possibleRoute)
+ }
+ if pathData.Route != nil {
+ routerLog.Debugf("Reverse: Reverse Storing recognized action path %s for route %#v\n", action, pathData.Route)
+ }
+ }
+
+ // Likely unknown route because of a wildcard, perform manual lookup
+ if pathData.Route != nil {
+ route := pathData.Route
+
+ // If the controller or method are wildcards we need to populate the argValues
+ controllerWildcard := route.ControllerName[0] == ':'
+ methodWildcard := route.MethodName[0] == ':'
+
+ // populate route arguments with the names
+ if controllerWildcard {
+ argValues[route.ControllerName[1:]] = pathData.ControllerName
+ }
+ if methodWildcard {
+ argValues[route.MethodName[1:]] = pathData.MethodName
+ }
+ // In theory all routes should be defined and pre-populated, the route controllers may not be though
+ // with wildcard routes
+ if pathData.TypeOfController == nil {
+ if controllerWildcard || methodWildcard {
+ if controller := ControllerTypeByName(pathData.ControllerNamespace+pathData.ControllerName, route.ModuleSource); controller != nil {
+ // Wildcard match boundary
+ pathData.TypeOfController = controller
+ // See if the path exists in the module based
+ } else {
+ routerLog.Errorf("Reverse: Controller %s not found in reverse lookup", pathData.ControllerNamespace+pathData.ControllerName)
+ return
+ }
+ }
+ }
+
+ if pathData.TypeOfController == nil {
+ routerLog.Errorf("Reverse: Controller %s not found in reverse lookup", pathData.ControllerNamespace+pathData.ControllerName)
+ return
+ }
+ var (
+ queryValues = make(url.Values)
+ pathElements = strings.Split(route.Path, "/")
+ )
+ for i, el := range pathElements {
+ if el == "" || (el[0] != ':' && el[0] != '*') {
+ continue
+ }
+ val, ok := pathData.FixedParamsByName[el[1:]]
+ if !ok {
+ val, ok = argValues[el[1:]]
+ }
+ if !ok {
+ val = "<nil>"
+ routerLog.Error("Reverse: reverse route missing route argument ", "argument", el[1:])
+ }
+ pathElements[i] = val
+ delete(argValues, el[1:])
+ continue
+ }
+
+ // Add any args that were not inserted into the path into the query string.
+ for k, v := range argValues {
+ queryValues.Set(k, v)
+ }
+
+ // Calculate the final URL and Method
+ urlPath := strings.Join(pathElements, "/")
+ if len(queryValues) > 0 {
+ urlPath += "?" + queryValues.Encode()
+ }
+
+ method := route.Method
+ star := false
+ if route.Method == "*" {
+ method = "GET"
+ star = true
+ }
+
+ //INFO.Printf("Reversing action %s to %s Using Route %#v",action,url,pathData.Route)
+
+ return &ActionDefinition{
+ URL: urlPath,
+ Method: method,
+ Star: star,
+ Action: action,
+ Args: argValues,
+ Host: "TODO",
+ }
+ }
+
+ routerLog.Error("Reverse: Failed to find controller for reverse route", "action", action, "arguments", argValues)
+ return nil
+}
+
+func RouterFilter(c *Controller, fc []Filter) {
+ // Figure out the Controller/Action
+ route := MainRouter.Route(c.Request)
+ if route == nil {
+ c.Result = c.NotFound("No matching route found: " + c.Request.GetRequestURI())
+ return
+ }
+
+ // The route may want to explicitly return a 404.
+ if route.Action == httpStatusCode {
+ c.Result = c.NotFound("(intentionally)")
+ return
+ }
+
+ // Set the action.
+ if err := c.SetTypeAction(route.ControllerName, route.MethodName, route.TypeOfController); err != nil {
+ c.Result = c.NotFound(err.Error())
+ return
+ }
+
+ // Add the route and fixed params to the Request Params.
+ c.Params.Route = route.Params
+ // Assign logger if from module
+ if c.Type.ModuleSource != nil && c.Type.ModuleSource != appModule {
+ c.Log = c.Type.ModuleSource.Log.New("ip", c.ClientIP,
+ "path", c.Request.URL.Path, "method", c.Request.Method)
+ }
+
+ // Add the fixed parameters mapped by name.
+ // TODO: Pre-calculate this mapping.
+ for i, value := range route.FixedParams {
+ if c.Params.Fixed == nil {
+ c.Params.Fixed = make(url.Values)
+ }
+ if i < len(c.MethodType.Args) {
+ arg := c.MethodType.Args[i]
+ c.Params.Fixed.Set(arg.Name, value)
+ } else {
+ routerLog.Warn("RouterFilter: Too many parameters to action", "action", route.Action, "value", value)
+ break
+ }
+ }
+
+ fc[0](c, fc[1:])
+}
+
+// HTTPMethodOverride overrides allowed http methods via form or browser param
+func HTTPMethodOverride(c *Controller, fc []Filter) {
+ // An array of HTTP verbs allowed.
+ verbs := []string{"POST", "PUT", "PATCH", "DELETE"}
+
+ method := strings.ToUpper(c.Request.Method)
+
+ if method == "POST" {
+ param := ""
+ if f, err := c.Request.GetForm(); err == nil {
+ param = strings.ToUpper(f.Get("_method"))
+ }
+
+ if len(param) > 0 {
+ override := false
+ // Check if param is allowed
+ for _, verb := range verbs {
+ if verb == param {
+ override = true
+ break
+ }
+ }
+
+ if override {
+ c.Request.Method = param
+ } else {
+ c.Response.Status = 405
+ c.Result = c.RenderError(&Error{
+ Title: "Method not allowed",
+ Description: "Method " + param + " is not allowed (valid: " + strings.Join(verbs, ", ") + ")",
+ })
+ return
+ }
+
+ }
+ }
+
+ fc[0](c, fc[1:]) // Execute the next filter stage.
+}
+
+func init() {
+ OnAppStart(func() {
+ MainRouter = NewRouter(filepath.Join(BasePath, "conf", "routes"))
+ err := MainRouter.Refresh()
+ if MainWatcher != nil && Config.BoolDefault("watch.routes", true) {
+ MainWatcher.Listen(MainRouter, MainRouter.path)
+ } else if err != nil {
+ // Not in dev mode and Route loading failed, we should crash.
+ routerLog.Panic("init: router initialize error", "error", err)
+ }
+ })
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+// Data-driven tests that check that a given routes-file line translates into
+// the expected Route object.
+var routeTestCases = map[string]*Route{
+ "get / Application.Index": {
+ Method: "GET",
+ Path: "/",
+ Action: "Application.Index",
+ FixedParams: []string{},
+ },
+
+ "post /app/:id Application.SaveApp": {
+ Method: "POST",
+ Path: "/app/:id",
+ Action: "Application.SaveApp",
+ FixedParams: []string{},
+ },
+
+ "get /app/ Application.List": {
+ Method: "GET",
+ Path: "/app/",
+ Action: "Application.List",
+ FixedParams: []string{},
+ },
+
+ `get /app/:appId/ Application.Show`: {
+ Method: "GET",
+ Path: `/app/:appId/`,
+ Action: "Application.Show",
+ FixedParams: []string{},
+ },
+
+ `get /app-wild/*appId/ Application.WildShow`: {
+ Method: "GET",
+ Path: `/app-wild/*appId/`,
+ Action: "Application.WildShow",
+ FixedParams: []string{},
+ },
+
+ `GET /public/:filepath Static.Serve("public")`: {
+ Method: "GET",
+ Path: "/public/:filepath",
+ Action: "Static.Serve",
+ FixedParams: []string{
+ "public",
+ },
+ },
+
+ `GET /javascript/:filepath Static.Serve("public/js")`: {
+ Method: "GET",
+ Path: "/javascript/:filepath",
+ Action: "Static.Serve",
+ FixedParams: []string{
+ "public",
+ },
+ },
+
+ "* /apps/:id/:action Application.:action": {
+ Method: "*",
+ Path: "/apps/:id/:action",
+ Action: "Application.:action",
+ FixedParams: []string{},
+ },
+
+ "* /:controller/:action :controller.:action": {
+ Method: "*",
+ Path: "/:controller/:action",
+ Action: ":controller.:action",
+ FixedParams: []string{},
+ },
+
+ `GET / Application.Index("Test", "Test2")`: {
+ Method: "GET",
+ Path: "/",
+ Action: "Application.Index",
+ FixedParams: []string{
+ "Test",
+ "Test2",
+ },
+ },
+}
+
+// Run the test cases above.
+func TestComputeRoute(t *testing.T) {
+ for routeLine, expected := range routeTestCases {
+ method, path, action, fixedArgs, found := parseRouteLine(routeLine)
+ if !found {
+ t.Error("Failed to parse route line:", routeLine)
+ continue
+ }
+ actual := NewRoute(appModule, method, path, action, fixedArgs, "", 0)
+ eq(t, "Method", actual.Method, expected.Method)
+ eq(t, "Path", actual.Path, expected.Path)
+ eq(t, "Action", actual.Action, expected.Action)
+ if t.Failed() {
+ t.Fatal("Failed on route:", routeLine)
+ }
+ }
+}
+
+// Router Tests
+
+const TestRoutes = `
+# This is a comment
+GET / Application.Index
+GET /test/ Application.Index("Test", "Test2")
+GET /app/:id/ Application.Show
+GET /app-wild/*id/ Application.WildShow
+POST /app/:id Application.Save
+PATCH /app/:id/ Application.Update
+PROPFIND /app/:id Application.WebDevMethodPropFind
+MKCOL /app/:id Application.WebDevMethodMkCol
+COPY /app/:id Application.WebDevMethodCopy
+MOVE /app/:id Application.WebDevMethodMove
+PROPPATCH /app/:id Application.WebDevMethodPropPatch
+LOCK /app/:id Application.WebDevMethodLock
+UNLOCK /app/:id Application.WebDevMethodUnLock
+TRACE /app/:id Application.WebDevMethodTrace
+PURGE /app/:id Application.CacheMethodPurge
+GET /javascript/:filepath App\Static.Serve("public/js")
+GET /public/*filepath Static.Serve("public")
+* /:controller/:action :controller.:action
+
+GET /favicon.ico 404
+`
+
+var routeMatchTestCases = map[*http.Request]*RouteMatch{
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Index",
+ FixedParams: []string{},
+ Params: map[string][]string{},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/test/"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Index",
+ FixedParams: []string{"Test", "Test2"},
+ Params: map[string][]string{},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Show",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "PATCH",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Update",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "POST",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Save",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/app/123/"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Show",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/public/css/style.css"},
+ }: {
+ ControllerName: "static",
+ MethodName: "Serve",
+ FixedParams: []string{"public"},
+ Params: map[string][]string{"filepath": {"css/style.css"}},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/javascript/sessvars.js"},
+ }: {
+ ControllerName: "static",
+ MethodName: "Serve",
+ FixedParams: []string{"public/js"},
+ Params: map[string][]string{"filepath": {"sessvars.js"}},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/Implicit/Route"},
+ }: {
+ ControllerName: "implicit",
+ MethodName: "Route",
+ FixedParams: []string{},
+ Params: map[string][]string{
+ "METHOD": {"GET"},
+ "controller": {"Implicit"},
+ "action": {"Route"},
+ },
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/favicon.ico"},
+ }: {
+ ControllerName: "",
+ MethodName: "",
+ Action: "404",
+ FixedParams: []string{},
+ Params: map[string][]string{},
+ },
+
+ {
+ Method: "POST",
+ URL: &url.URL{Path: "/app/123"},
+ Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}},
+ }: {
+ ControllerName: "application",
+ MethodName: "Update",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "GET",
+ URL: &url.URL{Path: "/app/123"},
+ Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}},
+ }: {
+ ControllerName: "application",
+ MethodName: "Show",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "PATCH",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "Update",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "PROPFIND",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodPropFind",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "MKCOL",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodMkCol",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "COPY",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodCopy",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "MOVE",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodMove",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "PROPPATCH",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodPropPatch",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+ {
+ Method: "LOCK",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodLock",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "UNLOCK",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodUnLock",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "TRACE",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "WebDevMethodTrace",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+
+ {
+ Method: "PURGE",
+ URL: &url.URL{Path: "/app/123"},
+ }: {
+ ControllerName: "application",
+ MethodName: "CacheMethodPurge",
+ FixedParams: []string{},
+ Params: map[string][]string{"id": {"123"}},
+ },
+}
+
+func TestRouteMatches(t *testing.T) {
+ initControllers()
+ BasePath = "/BasePath"
+ router := NewRouter("")
+ router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false)
+ if err := router.updateTree(); err != nil {
+ t.Errorf("updateTree failed: %s", err)
+ }
+ for req, expected := range routeMatchTestCases {
+ t.Log("Routing:", req.Method, req.URL)
+
+ context := NewGoContext(nil)
+ context.Request.SetRequest(req)
+ c := NewTestController(nil, req)
+
+ actual := router.Route(c.Request)
+ if !eq(t, "Found route", actual != nil, expected != nil) {
+ continue
+ }
+ if expected.ControllerName != "" {
+ eq(t, "ControllerName", actual.ControllerName, appModule.Namespace()+expected.ControllerName)
+ } else {
+ eq(t, "ControllerName", actual.ControllerName, expected.ControllerName)
+ }
+
+ eq(t, "MethodName", actual.MethodName, strings.ToLower(expected.MethodName))
+ eq(t, "len(Params)", len(actual.Params), len(expected.Params))
+ for key, actualValue := range actual.Params {
+ eq(t, "Params "+key, actualValue[0], expected.Params[key][0])
+ }
+ eq(t, "len(FixedParams)", len(actual.FixedParams), len(expected.FixedParams))
+ for i, actualValue := range actual.FixedParams {
+ eq(t, "FixedParams", actualValue, expected.FixedParams[i])
+ }
+ }
+}
+
+// Reverse Routing
+
+type ReverseRouteArgs struct {
+ action string
+ args map[string]string
+}
+
+var reverseRoutingTestCases = map[*ReverseRouteArgs]*ActionDefinition{
+ {
+ action: "Application.Index",
+ args: map[string]string{},
+ }: {
+ URL: "/",
+ Method: "GET",
+ Star: false,
+ Action: "Application.Index",
+ },
+
+ {
+ action: "Application.Show",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123/",
+ Method: "GET",
+ Star: false,
+ Action: "Application.Show",
+ },
+
+ {
+ action: "Implicit.Route",
+ args: map[string]string{},
+ }: {
+ URL: "/implicit/route",
+ Method: "GET",
+ Star: true,
+ Action: "Implicit.Route",
+ },
+
+ {
+ action: "Application.Save",
+ args: map[string]string{"id": "123", "c": "http://continue"},
+ }: {
+ URL: "/app/123?c=http%3A%2F%2Fcontinue",
+ Method: "POST",
+ Star: false,
+ Action: "Application.Save",
+ },
+
+ {
+ action: "Application.WildShow",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app-wild/123/",
+ Method: "GET",
+ Star: false,
+ Action: "Application.WildShow",
+ },
+
+ {
+ action: "Application.WebDevMethodPropFind",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "PROPFIND",
+ Star: false,
+ Action: "Application.WebDevMethodPropFind",
+ },
+ {
+ action: "Application.WebDevMethodMkCol",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "MKCOL",
+ Star: false,
+ Action: "Application.WebDevMethodMkCol",
+ },
+ {
+ action: "Application.WebDevMethodCopy",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "COPY",
+ Star: false,
+ Action: "Application.WebDevMethodCopy",
+ },
+ {
+ action: "Application.WebDevMethodMove",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "MOVE",
+ Star: false,
+ Action: "Application.WebDevMethodMove",
+ },
+ {
+ action: "Application.WebDevMethodPropPatch",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "PROPPATCH",
+ Star: false,
+ Action: "Application.WebDevMethodPropPatch",
+ },
+ {
+ action: "Application.WebDevMethodLock",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "LOCK",
+ Star: false,
+ Action: "Application.WebDevMethodLock",
+ },
+ {
+ action: "Application.WebDevMethodUnLock",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "UNLOCK",
+ Star: false,
+ Action: "Application.WebDevMethodUnLock",
+ },
+ {
+ action: "Application.WebDevMethodTrace",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "TRACE",
+ Star: false,
+ Action: "Application.WebDevMethodTrace",
+ },
+ {
+ action: "Application.CacheMethodPurge",
+ args: map[string]string{"id": "123"},
+ }: {
+ URL: "/app/123",
+ Method: "PURGE",
+ Star: false,
+ Action: "Application.CacheMethodPurge",
+ },
+}
+
+type testController struct {
+ *Controller
+}
+
+func initControllers() {
+ registerControllers()
+}
+func TestReverseRouting(t *testing.T) {
+ initControllers()
+ router := NewRouter("")
+ router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false)
+ for routeArgs, expected := range reverseRoutingTestCases {
+ actual := router.Reverse(routeArgs.action, routeArgs.args)
+ if !eq(t, fmt.Sprintf("Found route %s %s", routeArgs.action, actual), actual != nil, expected != nil) {
+ continue
+ }
+ eq(t, "Url", actual.URL, expected.URL)
+ eq(t, "Method", actual.Method, expected.Method)
+ eq(t, "Star", actual.Star, expected.Star)
+ eq(t, "Action", actual.Action, expected.Action)
+ }
+}
+
+func BenchmarkRouter(b *testing.B) {
+ router := NewRouter("")
+ router.Routes, _ = parseRoutes(nil, "", "", TestRoutes, false)
+ if err := router.updateTree(); err != nil {
+ b.Errorf("updateTree failed: %s", err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N/len(routeMatchTestCases); i++ {
+ for req := range routeMatchTestCases {
+ c := NewTestController(nil, req)
+ r := router.Route(c.Request)
+ if r == nil {
+ b.Errorf("Request not found: %s", req.URL.Path)
+ }
+ }
+ }
+}
+
+// The benchmark from github.com/ant0ine/go-urlrouter
+func BenchmarkLargeRouter(b *testing.B) {
+ router := NewRouter("")
+
+ routePaths := []string{
+ "/",
+ "/signin",
+ "/signout",
+ "/profile",
+ "/settings",
+ "/upload/*file",
+ }
+ for i := 0; i < 10; i++ {
+ for j := 0; j < 5; j++ {
+ routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id/property%d", i, j))
+ }
+ routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id", i))
+ routePaths = append(routePaths, fmt.Sprintf("/resource%d", i))
+ }
+ routePaths = append(routePaths, "/:any")
+
+ for _, p := range routePaths {
+ router.Routes = append(router.Routes,
+ NewRoute(appModule, "GET", p, "Controller.Action", "", "", 0))
+ }
+ if err := router.updateTree(); err != nil {
+ b.Errorf("updateTree failed: %s", err)
+ }
+
+ requestUrls := []string{
+ "http://example.org/",
+ "http://example.org/resource9/123",
+ "http://example.org/resource9/123/property1",
+ "http://example.org/doesnotexist",
+ }
+ var reqs []*http.Request
+ for _, url := range requestUrls {
+ req, _ := http.NewRequest("GET", url, nil)
+ reqs = append(reqs, req)
+ }
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N/len(reqs); i++ {
+ for _, req := range reqs {
+ c := NewTestController(nil, req)
+ route := router.Route(c.Request)
+ if route == nil {
+ b.Errorf("Failed to route: %s", req.URL.Path)
+ }
+ }
+ }
+}
+
+func BenchmarkRouterFilter(b *testing.B) {
+ startFakeBookingApp()
+ controllers := []*Controller{
+ NewTestController(nil, showRequest),
+ NewTestController(nil, staticRequest),
+ }
+ for _, c := range controllers {
+ c.Params = &Params{}
+ ParseParams(c.Params, c.Request)
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N/len(controllers); i++ {
+ for _, c := range controllers {
+ RouterFilter(c, NilChain)
+ }
+ }
+}
+
+func TestOverrideMethodFilter(t *testing.T) {
+ req, _ := http.NewRequest("POST", "/hotels/3", strings.NewReader("_method=put"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
+ c := NewTestController(nil, req)
+
+ if HTTPMethodOverride(c, NilChain); c.Request.Method != "PUT" {
+ t.Errorf("Expected to override current method '%s' in route, found '%s' instead", "", c.Request.Method)
+ }
+}
+
+// Helpers
+
+func eq(t *testing.T, name string, a, b interface{}) bool {
+ if a != b {
+ t.Error(name, ": (actual)", a, " != ", b, "(expected)")
+ return false
+ }
+ return true
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "errors"
+ "io"
+ "mime/multipart"
+ "net/url"
+ "strings"
+ "time"
+)
+
+const (
+ /* Minimum Engine Type Values */
+ _ = iota
+ ENGINE_RESPONSE_STATUS
+ ENGINE_WRITER
+ ENGINE_PARAMETERS
+ ENGINE_PATH
+ ENGINE_REQUEST
+ ENGINE_RESPONSE
+)
+const (
+ /* HTTP Engine Type Values Starts at 1000 */
+ HTTP_QUERY = ENGINE_PARAMETERS
+ HTTP_PATH = ENGINE_PATH
+ HTTP_BODY = iota + 1000
+ HTTP_FORM = iota + 1000
+ HTTP_MULTIPART_FORM = iota + 1000
+ HTTP_METHOD = iota + 1000
+ HTTP_REQUEST_URI = iota + 1000
+ HTTP_REQUEST_CONTEXT = iota + 1000
+ HTTP_REMOTE_ADDR = iota + 1000
+ HTTP_HOST = iota + 1000
+ HTTP_URL = iota + 1000
+ HTTP_SERVER_HEADER = iota + 1000
+ HTTP_STREAM_WRITER = iota + 1000
+ HTTP_WRITER = ENGINE_WRITER
+)
+
+type (
+ ServerContext interface {
+ GetRequest() ServerRequest
+ GetResponse() ServerResponse
+ }
+
+ // Callback ServerRequest type
+ ServerRequest interface {
+ GetRaw() interface{}
+ Get(theType int) (interface{}, error)
+ Set(theType int, theValue interface{}) bool
+ }
+ // Callback ServerResponse type
+ ServerResponse interface {
+ ServerRequest
+ }
+ // Callback WebSocket type
+ ServerWebSocket interface {
+ ServerResponse
+ MessageSendJSON(v interface{}) error
+ MessageReceiveJSON(v interface{}) error
+ MessageSend(v interface{}) error
+ MessageReceive(v interface{}) error
+ }
+
+ // Expected response for HTTP_SERVER_HEADER type (if implemented)
+ ServerHeader interface {
+ SetCookie(cookie string) // Sets the cookie
+ GetCookie(key string) (value ServerCookie, err error) // Gets the cookie
+ Set(key string, value string)
+ Add(key string, value string)
+ Del(key string)
+ Get(key string) (value []string)
+ GetKeys() (headerKeys []string)
+ SetStatus(statusCode int)
+ }
+
+ // Expected response for FROM_HTTP_COOKIE type (if implemented)
+ ServerCookie interface {
+ GetValue() string
+ }
+
+ // Expected response for HTTP_MULTIPART_FORM
+ ServerMultipartForm interface {
+ GetFiles() map[string][]*multipart.FileHeader
+ GetValues() url.Values
+ RemoveAll() error
+ }
+ StreamWriter interface {
+ WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error
+ }
+
+ ServerEngine interface {
+ // Initialize the server (non blocking)
+ Init(init *EngineInit)
+ // Starts the server. This will block until server is stopped
+ Start()
+ // Fires a new event to the server
+ Event(event Event, args interface{}) EventResponse
+ // Returns the engine instance for specific calls
+ Engine() interface{}
+ // Returns the engine Name
+ Name() string
+ // Returns any stats
+ Stats() map[string]interface{}
+ }
+
+ // The initialization structure passed into the engine
+ EngineInit struct {
+ Address, // The address
+ Network string // The network
+ Port int // The port
+ HTTPMuxList ServerMuxList // The HTTPMux
+ Callback func(ServerContext) // The ServerContext callback endpoint
+ }
+
+ // An empty server engine
+ ServerEngineEmpty struct {
+ }
+
+ // The route handler structure
+ ServerMux struct {
+ PathPrefix string // The path prefix
+ Callback interface{} // The callback interface as appropriate to the server
+ }
+
+ // A list of handlers used for adding special route functions
+ ServerMuxList []ServerMux
+)
+
+// Sorting function
+func (r ServerMuxList) Len() int {
+ return len(r)
+}
+
+// Sorting function
+func (r ServerMuxList) Less(i, j int) bool {
+ return len(r[i].PathPrefix) > len(r[j].PathPrefix)
+}
+
+// Sorting function
+func (r ServerMuxList) Swap(i, j int) {
+ r[i], r[j] = r[j], r[i]
+}
+
+// Search function, returns the largest path matching this
+func (r ServerMuxList) Find(path string) (interface{}, bool) {
+ for _, p := range r {
+ if p.PathPrefix == path || strings.HasPrefix(path, p.PathPrefix) {
+ return p.Callback, true
+ }
+ }
+ return nil, false
+}
+
+// Adds this routehandler to the route table. It will be called (if the path prefix matches)
+// before the Revel mux, this can only be called after the ENGINE_BEFORE_INITIALIZED event
+func AddHTTPMux(path string, callback interface{}) {
+ ServerEngineInit.HTTPMuxList = append(ServerEngineInit.HTTPMuxList, ServerMux{PathPrefix: path, Callback: callback})
+}
+
+// Callback point for the server to handle the
+func handleInternal(ctx ServerContext) {
+ start := time.Now()
+ var c *Controller
+ if RevelConfig.Controller.Reuse {
+ c = RevelConfig.Controller.Stack.Pop().(*Controller)
+ defer func() {
+ RevelConfig.Controller.Stack.Push(c)
+ }()
+ } else {
+ c = NewControllerEmpty()
+ }
+
+ var (
+
+ req, resp = c.Request, c.Response
+ )
+ c.SetController(ctx)
+ req.WebSocket, _ = ctx.GetResponse().(ServerWebSocket)
+
+ clientIP := ClientIP(req)
+
+ // Once finished in the internal, we can return these to the stack
+
+ c.ClientIP = clientIP
+ c.Log = AppLog.New("ip", clientIP,
+ "path", req.GetPath(), "method", req.Method)
+ // Call the first filter, this will process the request
+ Filters[0](c, Filters[1:])
+ if c.Result != nil {
+ c.Result.Apply(req, resp)
+ } else if c.Response.Status != 0 {
+ c.Response.SetStatus(c.Response.Status)
+ }
+ // Close the Writer if we can
+ if w, ok := resp.GetWriter().(io.Closer); ok {
+ _ = w.Close()
+ }
+
+ // Revel request access log format
+ // RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath
+ // Sample format: terminal format
+ // INFO 2017/08/02 22:31:41 server-engine.go:168: Request Stats ip=::1 path=/public/img/favicon.png method=GET action=Static.Serve namespace=static\\ start=2017/08/02 22:31:41 status=200 duration_seconds=0.0007656
+ // Recommended storing format to json code which looks like
+ // {"action":"Static.Serve","caller":"server-engine.go:168","duration_seconds":0.00058336,"ip":"::1","lvl":3,
+ // "method":"GET","msg":"Request Stats","namespace":"static\\","path":"/public/img/favicon.png",
+ // "start":"2017-08-02T22:34:08-0700","status":200,"t":"2017-08-02T22:34:08.303112145-07:00"}
+
+ c.Log.Info("Request Stats",
+ "start", start,
+ "status", c.Response.Status,
+ "duration_seconds", time.Since(start).Seconds(), "section", "requestlog",
+ )
+}
+
+var (
+ ENGINE_UNKNOWN_GET = errors.New("Server Engine Invalid Get")
+)
+
+func (e *ServerEngineEmpty) Get(_ string) interface{} {
+ return nil
+}
+func (e *ServerEngineEmpty) Set(_ string, _ interface{}) bool {
+ return false
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "github.com/revel/revel/session"
+ "os"
+ "strconv"
+ "strings"
+ "github.com/revel/revel/utils"
+)
+
+// Revel's variables server, router, etc
+var (
+ MainRouter *Router
+ MainTemplateLoader *TemplateLoader
+ MainWatcher *Watcher
+ serverEngineMap = map[string]func() ServerEngine{}
+ CurrentEngine ServerEngine
+ ServerEngineInit *EngineInit
+ serverLogger = RevelLog.New("section", "server")
+)
+
+func RegisterServerEngine(name string, loader func() ServerEngine) {
+ serverLogger.Debug("RegisterServerEngine: Registered engine ", "name", name)
+ serverEngineMap[name] = loader
+}
+
+// InitServer initializes the server and returns the handler
+// It can be used as an alternative entry-point if one needs the http handler
+// to be exposed. E.g. to run on multiple addresses and ports or to set custom
+// TLS options.
+func InitServer() {
+ CurrentEngine.Init(ServerEngineInit)
+ initControllerStack()
+ startupHooks.Run()
+
+ // Load templates
+ MainTemplateLoader = NewTemplateLoader(TemplatePaths)
+ if err := MainTemplateLoader.Refresh(); err != nil {
+ serverLogger.Debug("InitServer: Main template loader failed to refresh", "error", err)
+ }
+
+ // The "watch" config variable can turn on and off all watching.
+ // (As a convenient way to control it all together.)
+ if Config.BoolDefault("watch", true) {
+ MainWatcher = NewWatcher()
+ Filters = append([]Filter{WatchFilter}, Filters...)
+ }
+
+ // If desired (or by default), create a watcher for templates and routes.
+ // The watcher calls Refresh() on things on the first request.
+ if MainWatcher != nil && Config.BoolDefault("watch.templates", true) {
+ MainWatcher.Listen(MainTemplateLoader, MainTemplateLoader.paths...)
+ }
+
+}
+
+// Run the server.
+// This is called from the generated main file.
+// If port is non-zero, use that. Else, read the port from app.conf.
+func Run(port int) {
+ defer func() {
+ if r := recover(); r != nil {
+ RevelLog.Crit("Recovered error in startup", "error", r)
+ RaiseEvent(REVEL_FAILURE, r)
+ panic("Fatal error in startup")
+ }
+ }()
+
+ // Initialize the session logger, must be initiated from this app to avoid
+ // circular references
+ session.InitSession(RevelLog)
+
+ // Create the CurrentEngine instance from the application config
+ InitServerEngine(port, Config.StringDefault("server.engine", GO_NATIVE_SERVER_ENGINE))
+ RaiseEvent(ENGINE_BEFORE_INITIALIZED, nil)
+ InitServer()
+ RaiseEvent(ENGINE_STARTED, nil)
+ // This is needed for the harness to recognize that the server is started, it looks for the word
+ // "Listening" in the stdout stream
+
+ fmt.Fprintf(os.Stdout, "Revel engine is listening on.. %s\n", ServerEngineInit.Address)
+ // Start never returns,
+ CurrentEngine.Start()
+ fmt.Fprintf(os.Stdout, "Revel engine is NOT listening on.. %s\n", ServerEngineInit.Address)
+ RaiseEvent(ENGINE_SHUTDOWN, nil)
+ shutdownHooks.Run()
+ println("\nRevel exited normally\n")
+}
+
+// Build an engine initialization object and start the engine
+func InitServerEngine(port int, serverEngine string) {
+ address := HTTPAddr
+ if address == "" {
+ address = "localhost"
+ }
+ if port == 0 {
+ port = HTTPPort
+ }
+
+ var network = "tcp"
+ var localAddress string
+
+ // If the port is zero, treat the address as a fully qualified local address.
+ // This address must be prefixed with the network type followed by a colon,
+ // e.g. unix:/tmp/app.socket or tcp6:::1 (equivalent to tcp6:0:0:0:0:0:0:0:1)
+ if port == 0 {
+ parts := strings.SplitN(address, ":", 2)
+ network = parts[0]
+ localAddress = parts[1]
+ } else {
+ localAddress = address + ":" + strconv.Itoa(port)
+ }
+
+ if engineLoader, ok := serverEngineMap[serverEngine]; !ok {
+ panic("Server Engine " + serverEngine + " Not found")
+ } else {
+ CurrentEngine = engineLoader()
+ serverLogger.Debug("InitServerEngine: Found server engine and invoking", "name", CurrentEngine.Name())
+ ServerEngineInit = &EngineInit{
+ Address: localAddress,
+ Network: network,
+ Port: port,
+ Callback: handleInternal,
+ }
+ }
+ AddInitEventHandler(CurrentEngine.Event)
+}
+
+// Initialize the controller stack for the application
+func initControllerStack() {
+ RevelConfig.Controller.Reuse = Config.BoolDefault("revel.controller.reuse",true)
+
+ if RevelConfig.Controller.Reuse {
+ RevelConfig.Controller.Stack = utils.NewStackLock(
+ Config.IntDefault("revel.controller.stack", 10),
+ Config.IntDefault("revel.controller.maxstack", 200), func() interface{} {
+ return NewControllerEmpty()
+ })
+ RevelConfig.Controller.CachedStackSize = Config.IntDefault("revel.cache.controller.stack", 10)
+ RevelConfig.Controller.CachedStackMaxSize = Config.IntDefault("revel.cache.controller.maxstack", 100)
+ RevelConfig.Controller.CachedMap = map[string]*utils.SimpleLockStack{}
+ }
+}
+
+// Called to stop the server
+func StopServer(value interface{}) EventResponse {
+ return RaiseEvent(ENGINE_SHUTDOWN_REQUEST,value)
+}
\ No newline at end of file
--- /dev/null
+package revel
+
+import (
+ "net"
+ "net/http"
+ "time"
+ "context"
+ "golang.org/x/net/websocket"
+ "io"
+ "mime/multipart"
+ "net/url"
+ "os"
+ "os/signal"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+ "github.com/revel/revel/utils"
+)
+
+// Register the GoHttpServer engine
+func init() {
+ AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+ if typeOf == REVEL_BEFORE_MODULES_LOADED {
+ RegisterServerEngine(GO_NATIVE_SERVER_ENGINE, func() ServerEngine { return &GoHttpServer{} })
+ }
+ return
+ })
+}
+
+// The Go HTTP server
+type GoHttpServer struct {
+ Server *http.Server // The server instance
+ ServerInit *EngineInit // The server engine initialization
+ MaxMultipartSize int64 // The largest size of file to accept
+ goContextStack *utils.SimpleLockStack // The context stack Set via server.context.stack, server.context.maxstack
+ goMultipartFormStack *utils.SimpleLockStack // The multipart form stack set via server.form.stack, server.form.maxstack
+ HttpMuxList ServerMuxList
+ HasAppMux bool
+ signalChan chan os.Signal
+}
+
+// Called to initialize the server with this EngineInit
+func (g *GoHttpServer) Init(init *EngineInit) {
+ g.MaxMultipartSize = int64(Config.IntDefault("server.request.max.multipart.filesize", 32)) << 20 /* 32 MB */
+ g.goContextStack = utils.NewStackLock(Config.IntDefault("server.context.stack", 100),
+ Config.IntDefault("server.context.maxstack", 200),
+ func() interface{} {
+ return NewGoContext(g)
+ })
+ g.goMultipartFormStack = utils.NewStackLock(Config.IntDefault("server.form.stack", 100),
+ Config.IntDefault("server.form.maxstack", 200),
+ func() interface{} { return &GoMultipartForm{} })
+ g.ServerInit = init
+
+ revelHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ g.Handle(writer, request)
+ })
+
+ // Adds the mux list
+ g.HttpMuxList = init.HTTPMuxList
+ sort.Sort(g.HttpMuxList)
+ g.HasAppMux = len(g.HttpMuxList) > 0
+ g.signalChan = make(chan os.Signal)
+
+ g.Server = &http.Server{
+ Addr: init.Address,
+ Handler: revelHandler,
+ ReadTimeout: time.Duration(Config.IntDefault("http.timeout.read", 0)) * time.Second,
+ WriteTimeout: time.Duration(Config.IntDefault("http.timeout.write", 0)) * time.Second,
+ }
+
+}
+
+// Handler is assigned in the Init
+func (g *GoHttpServer) Start() {
+ go func() {
+ time.Sleep(100 * time.Millisecond)
+ serverLogger.Debugf("Start: Listening on %s...", g.Server.Addr)
+ }()
+ if HTTPSsl {
+ if g.ServerInit.Network != "tcp" {
+ // This limitation is just to reduce complexity, since it is standard
+ // to terminate SSL upstream when using unix domain sockets.
+ serverLogger.Fatal("SSL is only supported for TCP sockets. Specify a port to listen on.")
+ }
+ serverLogger.Fatal("Failed to listen:", "error",
+ g.Server.ListenAndServeTLS(HTTPSslCert, HTTPSslKey))
+ } else {
+ listener, err := net.Listen(g.ServerInit.Network, g.Server.Addr)
+ if err != nil {
+ serverLogger.Fatal("Failed to listen:", "error", err)
+ }
+ serverLogger.Warn("Server exiting:", "error", g.Server.Serve(listener))
+ }
+}
+
+// Handle the request and response for the server
+func (g *GoHttpServer) Handle(w http.ResponseWriter, r *http.Request) {
+ // This section is called if the developer has added custom mux to the app
+ if g.HasAppMux && g.handleAppMux(w, r) {
+ return
+ }
+ g.handleMux(w, r)
+}
+
+// Handle the request and response for the servers mux
+func (g *GoHttpServer) handleAppMux(w http.ResponseWriter, r *http.Request) bool {
+ // Check the prefix and split them
+ requestPath := path.Clean(r.URL.Path)
+ if handler, hasHandler := g.HttpMuxList.Find(requestPath); hasHandler {
+ clientIP := HttpClientIP(r)
+ localLog := AppLog.New("ip", clientIP,
+ "path", r.URL.Path, "method", r.Method)
+ defer func() {
+ if err := recover(); err != nil {
+ localLog.Error("An error was caught using the handler", "path", requestPath, "error", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ }()
+ start := time.Now()
+ handler.(http.HandlerFunc)(w, r)
+ localLog.Info("Request Stats",
+ "start", start,
+ "duration_seconds", time.Since(start).Seconds(), "section", "requestlog",
+ )
+ return true
+ }
+ return false
+}
+
+// Passes the server request to Revel
+func (g *GoHttpServer) handleMux(w http.ResponseWriter, r *http.Request) {
+ if maxRequestSize := int64(Config.IntDefault("http.maxrequestsize", 0)); maxRequestSize > 0 {
+ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+ }
+
+ upgrade := r.Header.Get("Upgrade")
+ context := g.goContextStack.Pop().(*GoContext)
+ defer func() {
+ g.goContextStack.Push(context)
+ }()
+ context.Request.SetRequest(r)
+ context.Response.SetResponse(w)
+
+ if upgrade == "websocket" || upgrade == "Websocket" {
+ websocket.Handler(func(ws *websocket.Conn) {
+ //Override default Read/Write timeout with sane value for a web socket request
+ if err := ws.SetDeadline(time.Now().Add(time.Hour * 24)); err != nil {
+ serverLogger.Error("SetDeadLine failed:", err)
+ }
+ r.Method = "WS"
+ context.Request.WebSocket = ws
+ context.WebSocket = &GoWebSocket{Conn: ws, GoResponse: *context.Response}
+ g.ServerInit.Callback(context)
+ }).ServeHTTP(w, r)
+ } else {
+ g.ServerInit.Callback(context)
+ }
+}
+
+// ClientIP method returns client IP address from HTTP request.
+//
+// Note: Set property "app.behind.proxy" to true only if Revel is running
+// behind proxy like nginx, haproxy, apache, etc. Otherwise
+// you may get inaccurate Client IP address. Revel parses the
+// IP address in the order of X-Forwarded-For, X-Real-IP.
+//
+// By default revel will get http.Request's RemoteAddr
+func HttpClientIP(r *http.Request) string {
+ if Config.BoolDefault("app.behind.proxy", false) {
+ // Header X-Forwarded-For
+ if fwdFor := strings.TrimSpace(r.Header.Get(HdrForwardedFor)); fwdFor != "" {
+ index := strings.Index(fwdFor, ",")
+ if index == -1 {
+ return fwdFor
+ }
+ return fwdFor[:index]
+ }
+
+ // Header X-Real-Ip
+ if realIP := strings.TrimSpace(r.Header.Get(HdrRealIP)); realIP != "" {
+ return realIP
+ }
+ }
+
+ if remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+ return remoteAddr
+ }
+
+ return ""
+}
+
+// The server key name
+const GO_NATIVE_SERVER_ENGINE = "go"
+
+// Returns the name of this engine
+func (g *GoHttpServer) Name() string {
+ return GO_NATIVE_SERVER_ENGINE
+}
+
+// Returns stats for this engine
+func (g *GoHttpServer) Stats() map[string]interface{} {
+ return map[string]interface{}{
+ "Go Engine Context": g.goContextStack.String(),
+ "Go Engine Forms": g.goMultipartFormStack.String(),
+ }
+}
+
+// Return the engine instance
+func (g *GoHttpServer) Engine() interface{} {
+ return g.Server
+}
+
+// Handles an event from Revel
+func (g *GoHttpServer) Event(event Event, args interface{}) (r EventResponse) {
+ switch event {
+ case ENGINE_STARTED:
+ signal.Notify(g.signalChan, os.Interrupt, os.Kill)
+ go func() {
+ _ = <-g.signalChan
+ serverLogger.Info("Received quit singal Please wait ... ")
+ RaiseEvent(ENGINE_SHUTDOWN_REQUEST, nil)
+ }()
+ case ENGINE_SHUTDOWN_REQUEST:
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(Config.IntDefault("app.cancel.timeout", 60)))
+ defer cancel()
+ g.Server.Shutdown(ctx)
+ default:
+
+ }
+
+ return
+}
+
+type (
+ // The go context
+ GoContext struct {
+ Request *GoRequest // The request
+ Response *GoResponse // The response
+ WebSocket *GoWebSocket // The websocket
+ }
+
+ // The go request
+ GoRequest struct {
+ Original *http.Request // The original
+ FormParsed bool // True if form parsed
+ MultiFormParsed bool // True if multipart form parsed
+ WebSocket *websocket.Conn // The websocket
+ ParsedForm *GoMultipartForm // The parsed form data
+ Goheader *GoHeader // The header
+ Engine *GoHttpServer // THe server
+ }
+
+ // The response
+ GoResponse struct {
+ Original http.ResponseWriter // The original writer
+ Goheader *GoHeader // The header
+ Writer io.Writer // The writer
+ Request *GoRequest // The request
+ Engine *GoHttpServer // The engine
+ }
+
+ // The multipart form
+ GoMultipartForm struct {
+ Form *multipart.Form // The form
+ }
+
+ // The go header
+ GoHeader struct {
+ Source interface{} // The source
+ isResponse bool // True if response header
+ }
+
+ // The websocket
+ GoWebSocket struct {
+ Conn *websocket.Conn // The connection
+ GoResponse // The response
+ }
+
+ // The cookie
+ GoCookie http.Cookie
+)
+
+// Create a new go context
+func NewGoContext(instance *GoHttpServer) *GoContext {
+ // This bit in here is for the test cases, which pass in a nil value
+ if instance == nil {
+ instance = &GoHttpServer{MaxMultipartSize: 32 << 20}
+ instance.goContextStack = utils.NewStackLock(100, 200,
+ func() interface{} {
+ return NewGoContext(instance)
+ })
+ instance.goMultipartFormStack = utils.NewStackLock(100, 200,
+ func() interface{} { return &GoMultipartForm{} })
+ }
+ c := &GoContext{Request: &GoRequest{Goheader: &GoHeader{}, Engine: instance}}
+ c.Response = &GoResponse{Goheader: &GoHeader{}, Request: c.Request, Engine: instance}
+ return c
+}
+
+// get the request
+func (c *GoContext) GetRequest() ServerRequest {
+ return c.Request
+}
+
+// Get the response
+func (c *GoContext) GetResponse() ServerResponse {
+ if c.WebSocket != nil {
+ return c.WebSocket
+ }
+ return c.Response
+}
+
+// Destroy the context
+func (c *GoContext) Destroy() {
+ c.Response.Destroy()
+ c.Request.Destroy()
+ if c.WebSocket != nil {
+ c.WebSocket.Destroy()
+ c.WebSocket = nil
+ }
+}
+
+// Communicate with the server engine to exchange data
+func (r *GoRequest) Get(key int) (value interface{}, err error) {
+ switch key {
+ case HTTP_SERVER_HEADER:
+ value = r.GetHeader()
+ case HTTP_MULTIPART_FORM:
+ value, err = r.GetMultipartForm()
+ case HTTP_QUERY:
+ value = r.Original.URL.Query()
+ case HTTP_FORM:
+ value, err = r.GetForm()
+ case HTTP_REQUEST_URI:
+ value = r.Original.URL.String()
+ case HTTP_REQUEST_CONTEXT:
+ value = r.Original.Context()
+ case HTTP_REMOTE_ADDR:
+ value = r.Original.RemoteAddr
+ case HTTP_METHOD:
+ value = r.Original.Method
+ case HTTP_PATH:
+ value = r.Original.URL.Path
+ case HTTP_HOST:
+ value = r.Original.Host
+ case HTTP_URL:
+ value = r.Original.URL
+ case HTTP_BODY:
+ value = r.Original.Body
+ default:
+ err = ENGINE_UNKNOWN_GET
+ }
+
+ return
+}
+
+// Sets the request key with value
+func (r *GoRequest) Set(key int, value interface{}) bool {
+ return false
+}
+
+// Returns the form
+func (r *GoRequest) GetForm() (url.Values, error) {
+ if !r.FormParsed {
+ if e := r.Original.ParseForm(); e != nil {
+ return nil, e
+ }
+ r.FormParsed = true
+ }
+
+ return r.Original.Form, nil
+}
+
+// Returns the form
+func (r *GoRequest) GetMultipartForm() (ServerMultipartForm, error) {
+ if !r.MultiFormParsed {
+ if e := r.Original.ParseMultipartForm(r.Engine.MaxMultipartSize); e != nil {
+ return nil, e
+ }
+ r.ParsedForm = r.Engine.goMultipartFormStack.Pop().(*GoMultipartForm)
+ r.ParsedForm.Form = r.Original.MultipartForm
+ }
+
+ return r.ParsedForm, nil
+}
+
+// Returns the header
+func (r *GoRequest) GetHeader() ServerHeader {
+ return r.Goheader
+}
+
+// Returns the raw value
+func (r *GoRequest) GetRaw() interface{} {
+ return r.Original
+}
+
+// Sets the request
+func (r *GoRequest) SetRequest(req *http.Request) {
+ r.Original = req
+ r.Goheader.Source = r
+ r.Goheader.isResponse = false
+
+}
+
+// Destroy the request
+func (r *GoRequest) Destroy() {
+ r.Goheader.Source = nil
+ r.Original = nil
+ r.FormParsed = false
+ r.MultiFormParsed = false
+ r.ParsedForm = nil
+}
+
+// Gets the key from the response
+func (r *GoResponse) Get(key int) (value interface{}, err error) {
+ switch key {
+ case HTTP_SERVER_HEADER:
+ value = r.Header()
+ case HTTP_STREAM_WRITER:
+ value = r
+ case HTTP_WRITER:
+ value = r.Writer
+ default:
+ err = ENGINE_UNKNOWN_GET
+ }
+ return
+}
+
+// Sets the key with the value
+func (r *GoResponse) Set(key int, value interface{}) (set bool) {
+ switch key {
+ case ENGINE_RESPONSE_STATUS:
+ r.Header().SetStatus(value.(int))
+ set = true
+ case HTTP_WRITER:
+ r.SetWriter(value.(io.Writer))
+ set = true
+ }
+ return
+}
+
+// Sets the header
+func (r *GoResponse) Header() ServerHeader {
+ return r.Goheader
+}
+
+// Gets the original response
+func (r *GoResponse) GetRaw() interface{} {
+ return r.Original
+}
+
+// Sets the writer
+func (r *GoResponse) SetWriter(writer io.Writer) {
+ r.Writer = writer
+}
+
+// Write output to stream
+func (r *GoResponse) WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error {
+ // Check to see if the output stream is modified, if not send it using the
+ // Native writer
+ written := false
+ if _, ok := r.Writer.(http.ResponseWriter); ok {
+ if rs, ok := reader.(io.ReadSeeker); ok {
+ http.ServeContent(r.Original, r.Request.Original, name, modtime, rs)
+ written = true
+ }
+ }
+ if !written {
+ // Else, do a simple io.Copy.
+ ius := r.Request.Original.Header.Get("If-Unmodified-Since")
+ if t, err := http.ParseTime(ius); err == nil && !modtime.IsZero() {
+ // The Date-Modified header truncates sub-second precision, so
+ // use mtime < t+1s instead of mtime <= t to check for unmodified.
+ if modtime.Before(t.Add(1 * time.Second)) {
+ h := r.Original.Header()
+ delete(h, "Content-Type")
+ delete(h, "Content-Length")
+ if h.Get("Etag") != "" {
+ delete(h, "Last-Modified")
+ }
+ r.Original.WriteHeader(http.StatusNotModified)
+ return nil
+ }
+ }
+
+ if contentlen != -1 {
+ header := ServerHeader(r.Goheader)
+ if writer, found := r.Writer.(*CompressResponseWriter); found {
+ header = ServerHeader(writer.Header)
+ }
+ header.Set("Content-Length", strconv.FormatInt(contentlen, 10))
+ }
+ if _, err := io.Copy(r.Writer, reader); err != nil {
+ r.Original.WriteHeader(http.StatusInternalServerError)
+ return err
+ } else {
+ r.Original.WriteHeader(http.StatusOK)
+ }
+ }
+ return nil
+}
+
+// Frees response
+func (r *GoResponse) Destroy() {
+ if c, ok := r.Writer.(io.Closer); ok {
+ c.Close()
+ }
+ r.Goheader.Source = nil
+ r.Original = nil
+ r.Writer = nil
+}
+
+// Sets the response
+func (r *GoResponse) SetResponse(w http.ResponseWriter) {
+ r.Original = w
+ r.Writer = w
+ r.Goheader.Source = r
+ r.Goheader.isResponse = true
+
+}
+
+// Sets the cookie
+func (r *GoHeader) SetCookie(cookie string) {
+ if r.isResponse {
+ r.Source.(*GoResponse).Original.Header().Add("Set-Cookie", cookie)
+ }
+}
+
+// Gets the cookie
+func (r *GoHeader) GetCookie(key string) (value ServerCookie, err error) {
+ if !r.isResponse {
+ var cookie *http.Cookie
+ if cookie, err = r.Source.(*GoRequest).Original.Cookie(key); err == nil {
+ value = GoCookie(*cookie)
+
+ }
+
+ }
+ return
+}
+
+// Sets (replaces) header key
+func (r *GoHeader) Set(key string, value string) {
+ if r.isResponse {
+ r.Source.(*GoResponse).Original.Header().Set(key, value)
+ }
+}
+
+// Adds the header key
+func (r *GoHeader) Add(key string, value string) {
+ if r.isResponse {
+ r.Source.(*GoResponse).Original.Header().Add(key, value)
+ }
+}
+
+// Deletes the header key
+func (r *GoHeader) Del(key string) {
+ if r.isResponse {
+ r.Source.(*GoResponse).Original.Header().Del(key)
+ }
+}
+
+// Gets the header key
+func (r *GoHeader) Get(key string) (value []string) {
+ if !r.isResponse {
+ value = r.Source.(*GoRequest).Original.Header[key]
+ if len(value) == 0 {
+ if ihead := r.Source.(*GoRequest).Original.Header.Get(key); ihead != "" {
+ value = append(value, ihead)
+ }
+ }
+ } else {
+ value = r.Source.(*GoResponse).Original.Header()[key]
+ }
+ return
+}
+
+// Returns list of header keys
+func (r *GoHeader) GetKeys() (value []string) {
+ if !r.isResponse {
+ for key := range r.Source.(*GoRequest).Original.Header {
+ value = append(value, key)
+ }
+ } else {
+ for key := range r.Source.(*GoResponse).Original.Header() {
+ value = append(value, key)
+ }
+ }
+ return
+}
+
+// Sets the status of the header
+func (r *GoHeader) SetStatus(statusCode int) {
+ if r.isResponse {
+ r.Source.(*GoResponse).Original.WriteHeader(statusCode)
+ }
+}
+
+// Return cookies value
+func (r GoCookie) GetValue() string {
+ return r.Value
+}
+
+// Return files from the form
+func (f *GoMultipartForm) GetFiles() map[string][]*multipart.FileHeader {
+ return f.Form.File
+}
+
+// Return values from the form
+func (f *GoMultipartForm) GetValues() url.Values {
+ return url.Values(f.Form.Value)
+}
+
+// Remove all values from the form freeing memory
+func (f *GoMultipartForm) RemoveAll() error {
+ return f.Form.RemoveAll()
+}
+
+/**
+ * Message send JSON
+ */
+func (g *GoWebSocket) MessageSendJSON(v interface{}) error {
+ return websocket.JSON.Send(g.Conn, v)
+}
+
+/**
+ * Message receive JSON
+ */
+func (g *GoWebSocket) MessageReceiveJSON(v interface{}) error {
+ return websocket.JSON.Receive(g.Conn, v)
+}
+
+/**
+ * Message Send
+ */
+func (g *GoWebSocket) MessageSend(v interface{}) error {
+ return websocket.Message.Send(g.Conn, v)
+}
+
+/**
+ * Message receive
+ */
+func (g *GoWebSocket) MessageReceive(v interface{}) error {
+ return websocket.Message.Receive(g.Conn, v)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+// This tries to benchmark the usual request-serving pipeline to get an overall
+// performance metric.
+//
+// Each iteration runs one mock request to display a hotel's detail page by id.
+//
+// Contributing parts:
+// - Routing
+// - Controller lookup / invocation
+// - Parameter binding
+// - Session, flash, i18n cookies
+// - Render() call magic
+// - Template rendering
+func BenchmarkServeAction(b *testing.B) {
+ benchmarkRequest(b, showRequest)
+}
+
+func BenchmarkServeJson(b *testing.B) {
+ benchmarkRequest(b, jsonRequest)
+}
+
+func BenchmarkServePlaintext(b *testing.B) {
+ benchmarkRequest(b, plaintextRequest)
+}
+
+// This tries to benchmark the static serving overhead when serving an "average
+// size" 7k file.
+func BenchmarkServeStatic(b *testing.B) {
+ benchmarkRequest(b, staticRequest)
+}
+
+func benchmarkRequest(b *testing.B, req *http.Request) {
+ startFakeBookingApp()
+ b.ResetTimer()
+ resp := httptest.NewRecorder()
+ for i := 0; i < b.N; i++ {
+ CurrentEngine.(*GoHttpServer).Handle(resp, req)
+ }
+}
+
+// Test that the booking app can be successfully run for a test.
+func TestFakeServer(t *testing.T) {
+ startFakeBookingApp()
+
+ resp := httptest.NewRecorder()
+
+ // First, test that the expected responses are actually generated
+ CurrentEngine.(*GoHttpServer).Handle(resp, showRequest)
+ if !strings.Contains(resp.Body.String(), "300 Main St.") {
+ t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+ t.FailNow()
+ }
+ resp.Body.Reset()
+
+ CurrentEngine.(*GoHttpServer).Handle(resp, staticRequest)
+ sessvarsSize := getFileSize(t, filepath.Join(BasePath, "public", "js", "sessvars.js"))
+ if int64(resp.Body.Len()) != sessvarsSize {
+ t.Errorf("Expected sessvars.js to have %d bytes, got %d:\n%s", sessvarsSize, resp.Body.Len(), resp.Body)
+ t.FailNow()
+ }
+ resp.Body.Reset()
+
+ CurrentEngine.(*GoHttpServer).Handle(resp, jsonRequest)
+ if !strings.Contains(resp.Body.String(), `"Address":"300 Main St."`) {
+ t.Errorf("Failed to find hotel address in JSON response:\n%s", resp.Body)
+ t.FailNow()
+ }
+ resp.Body.Reset()
+
+ CurrentEngine.(*GoHttpServer).Handle(resp, plaintextRequest)
+ if resp.Body.String() != "Hello, World!" {
+ t.Errorf("Failed to find greeting in plaintext response:\n%s", resp.Body)
+ t.FailNow()
+ }
+
+ resp.Body = nil
+}
+
+func getFileSize(t *testing.T, name string) int64 {
+ fi, err := os.Stat(name)
+ if err != nil {
+ t.Errorf("Unable to stat file:\n%s", name)
+ t.FailNow()
+ }
+ return fi.Size()
+}
+
+// Ensure on app start runs in order
+func TestOnAppStart(t *testing.T) {
+ str := ""
+ a := assert.New(t)
+ OnAppStart(func() {
+ str += " World"
+ }, 2)
+
+ OnAppStart(func() {
+ str += "Hello"
+ }, 1)
+
+ startFakeBookingApp()
+
+ a.Equal("Hello World", str, "Failed to order OnAppStart")
+}
+
+// Ensure on app stop runs in order
+func TestOnAppStop(t *testing.T) {
+ a := assert.New(t)
+ startFakeBookingApp()
+ i := ""
+ OnAppStop(func() {
+ i += "cruel world"
+ t.Logf("i: %v \n", i)
+ }, 2)
+ OnAppStop(func() {
+ i += "goodbye "
+ t.Logf("i: %v \n", i)
+ }, 1)
+ go func() {
+ time.Sleep(2 * time.Second)
+ RaiseEvent(ENGINE_SHUTDOWN_REQUEST, nil)
+ }()
+ Run(0)
+ a.Equal("goodbye cruel world", i, "Did not get shutdown events")
+
+}
+
+var (
+ showRequest, _ = http.NewRequest("GET", "/hotels/3", nil)
+ staticRequest, _ = http.NewRequest("GET", "/public/js/sessvars.js", nil)
+ jsonRequest, _ = http.NewRequest("GET", "/hotels/3/booking", nil)
+ plaintextRequest, _ = http.NewRequest("GET", "/hotels", nil)
+)
--- /dev/null
+package session
+
+// The logger for the session
+import "github.com/revel/revel/logger"
+
+var sessionLog logger.MultiLogger
+
+func InitSession(coreLogger logger.MultiLogger) {
+ sessionLog = coreLogger.New("section", "session")
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "github.com/twinj/uuid"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ // The key for the identity of the session
+ SessionIDKey = "_ID"
+ // The expiration date of the session
+ TimestampKey = "_TS"
+ // The value name indicating how long the session should persist - ie should it persist after the browser closes
+ // this is set under the TimestampKey if the session data should expire immediately
+ SessionValueName = "session"
+ // The key container for the json objects of the data, any non strings found in the map will be placed in here
+ // serialized by key using JSON
+ SessionObjectKeyName = "_object_"
+ // The mapped session object
+ SessionMapKeyName = "_map_"
+ // The suffix of the session cookie
+ SessionCookieSuffix = "_SESSION"
+)
+
+// Session data, can be any data, there are reserved keywords used by the storage data
+// SessionIDKey Is the key name for the session
+// TimestampKey Is the time that the session should expire
+//
+type Session map[string]interface{}
+
+func NewSession() Session {
+ return Session{}
+}
+
+// ID retrieves from the cookie or creates a time-based UUID identifying this
+// session.
+func (s Session) ID() string {
+ if sessionIDStr, ok := s[SessionIDKey]; ok {
+ return sessionIDStr.(string)
+ }
+
+ buffer := uuid.NewV4()
+
+ s[SessionIDKey] = hex.EncodeToString(buffer.Bytes())
+ return s[SessionIDKey].(string)
+}
+
+// getExpiration return a time.Time with the session's expiration date.
+// It uses the passed in expireAfterDuration to add with the current time if the timeout is not
+// browser dependent (ie session). If previous session has set to "session", the time returned is time.IsZero()
+func (s Session) GetExpiration(expireAfterDuration time.Duration) time.Time {
+ if expireAfterDuration == 0 || s[TimestampKey] == SessionValueName {
+ // Expire after closing browser
+ return time.Time{}
+ }
+ return time.Now().Add(expireAfterDuration)
+}
+
+// SetNoExpiration sets session to expire when browser session ends
+func (s Session) SetNoExpiration() {
+ s[TimestampKey] = SessionValueName
+}
+
+// SetDefaultExpiration sets session to expire after default duration
+func (s Session) SetDefaultExpiration() {
+ delete(s, TimestampKey)
+}
+
+// sessionTimeoutExpiredOrMissing returns a boolean of whether the session
+// cookie is either not present or present but beyond its time to live; i.e.,
+// whether there is not a valid session.
+func (s Session) SessionTimeoutExpiredOrMissing() bool {
+ if exp, present := s[TimestampKey]; !present {
+ return true
+ } else if exp == SessionValueName {
+ return false
+ } else if expInt, _ := strconv.Atoi(exp.(string)); int64(expInt) < time.Now().Unix() {
+ return true
+ }
+ return false
+}
+
+// Constant error if session value is not found
+var SESSION_VALUE_NOT_FOUND = errors.New("Session value not found")
+
+// Get an object or property from the session
+// it may be embedded inside the session.
+func (s Session) Get(key string) (newValue interface{}, err error) {
+ // First check to see if it is in the session
+ if v, found := s[key]; found {
+ return v, nil
+ }
+ return s.GetInto(key, nil, false)
+}
+
+// Get into the specified value.
+// If value exists in the session it will just return the value
+func (s Session) GetInto(key string, target interface{}, force bool) (result interface{}, err error) {
+ if v, found := s[key]; found && !force {
+ return v, nil
+ }
+ splitKey := strings.Split(key, ".")
+ rootKey := splitKey[0]
+
+ // Force always recreates the object from the session data map
+ if force {
+ if target == nil {
+ if result, err = s.sessionDataFromMap(key); err != nil {
+ return
+ }
+ } else if result, err = s.sessionDataFromObject(rootKey, target); err != nil {
+ return
+ }
+
+ return s.getNestedProperty(splitKey, result)
+ }
+
+ // Attempt to find the key in the session, this is the most generalized form
+ v, found := s[rootKey]
+ if !found {
+ if target == nil {
+ // Try to fetch it from the session
+
+ if v, err = s.sessionDataFromMap(rootKey); err != nil {
+ return
+ }
+ } else if v, err = s.sessionDataFromObject(rootKey, target); err != nil {
+ return
+ }
+ }
+
+ return s.getNestedProperty(splitKey, v)
+}
+
+// Returns the default value if the key is not found
+func (s Session) GetDefault(key string, value interface{}, defaultValue interface{}) interface{} {
+ v, e := s.GetInto(key, value, false)
+ if e != nil {
+ v = defaultValue
+ }
+ return v
+}
+
+// Extract the values from the session
+func (s Session) GetProperty(key string, value interface{}) (interface{}, error) {
+ // Capitalize the first letter
+ key = strings.Title(key)
+
+ sessionLog.Info("getProperty", "key", key, "value", value)
+
+ // For a map it is easy
+ if reflect.TypeOf(value).Kind() == reflect.Map {
+ val := reflect.ValueOf(value)
+ valueOf := val.MapIndex(reflect.ValueOf(key))
+ if valueOf == reflect.Zero(reflect.ValueOf(value).Type()) {
+ return nil, nil
+ }
+ //idx := val.MapIndex(reflect.ValueOf(key))
+ if !valueOf.IsValid() {
+ return nil, nil
+ }
+
+ return valueOf.Interface(), nil
+ }
+
+ objValue := s.reflectValue(value)
+ field := objValue.FieldByName(key)
+ if !field.IsValid() {
+ return nil, SESSION_VALUE_NOT_FOUND
+ }
+
+ return field.Interface(), nil
+}
+
+// Places the object into the session, a nil value will cause remove the key from the session
+// (or you can use the Session.Del(key) function
+func (s Session) Set(key string, value interface{}) error {
+ if value == nil {
+ s.Del(key)
+ return nil
+ }
+
+ s[key] = value
+ return nil
+}
+
+// Delete the key from the sessionObjects and Session
+func (s Session) Del(key string) {
+ sessionJsonMap := s.getSessionJsonMap()
+ delete(sessionJsonMap, key)
+ delete(s, key)
+}
+
+// Extracts the session as a map of [string keys] and json values
+func (s Session) getSessionJsonMap() map[string]string {
+ if sessionJson, found := s[SessionObjectKeyName]; found {
+ if _, valid := sessionJson.(map[string]string); !valid {
+ sessionLog.Error("Session object key corrupted, reset", "was", sessionJson)
+ s[SessionObjectKeyName] = map[string]string{}
+ }
+ // serialized data inside the session _objects
+ } else {
+ s[SessionObjectKeyName] = map[string]string{}
+ }
+
+ return s[SessionObjectKeyName].(map[string]string)
+}
+
+// Convert the map to a simple map[string]string map
+// this will marshal any non string objects encountered and store them the the jsonMap
+// The expiration time will also be assigned
+func (s Session) Serialize() map[string]string {
+ sessionJsonMap := s.getSessionJsonMap()
+ newMap := map[string]string{}
+ newObjectMap := map[string]string{}
+ for key, value := range sessionJsonMap {
+ newObjectMap[key] = value
+ }
+ for key, value := range s {
+ if key == SessionObjectKeyName || key == SessionMapKeyName {
+ continue
+ }
+ if reflect.ValueOf(value).Kind() == reflect.String {
+ newMap[key] = value.(string)
+ continue
+ }
+ println("Serialize the data for", key)
+ if data, err := json.Marshal(value); err != nil {
+ sessionLog.Error("Unable to marshal session ", "key", key, "error", err)
+ continue
+ } else {
+ newObjectMap[key] = string(data)
+ }
+ }
+ if len(newObjectMap) > 0 {
+ if data, err := json.Marshal(newObjectMap); err != nil {
+ sessionLog.Error("Unable to marshal session ", "key", SessionObjectKeyName, "error", err)
+
+ } else {
+ newMap[SessionObjectKeyName] = string(data)
+ }
+ }
+
+ return newMap
+}
+
+// Set the session object from the loaded data
+func (s Session) Load(data map[string]string) {
+ for key, value := range data {
+ if key == SessionObjectKeyName {
+ target := map[string]string{}
+ if err := json.Unmarshal([]byte(value), &target); err != nil {
+ sessionLog.Error("Unable to unmarshal session ", "key", SessionObjectKeyName, "error", err)
+ } else {
+ s[key] = target
+ }
+ } else {
+ s[key] = value
+ }
+
+ }
+}
+
+// Checks to see if the session is empty
+func (s Session) Empty() bool {
+ i := 0
+ for k := range s {
+ i++
+ if k == SessionObjectKeyName || k == SessionMapKeyName {
+ continue
+ }
+ }
+ return i == 0
+}
+
+func (s *Session) reflectValue(obj interface{}) reflect.Value {
+ var val reflect.Value
+
+ if reflect.TypeOf(obj).Kind() == reflect.Ptr {
+ val = reflect.ValueOf(obj).Elem()
+ } else {
+ val = reflect.ValueOf(obj)
+ }
+
+ return val
+}
+
+// Starting at position 1 drill into the object
+func (s Session) getNestedProperty(keys []string, newValue interface{}) (result interface{}, err error) {
+ for x := 1; x < len(keys); x++ {
+ newValue, err = s.GetProperty(keys[x], newValue)
+ if err != nil || newValue == nil {
+ return newValue, err
+ }
+ }
+ return newValue, nil
+}
+
+// Always converts the data from the session mapped objects into the target,
+// it will store the results under the session key name SessionMapKeyName
+func (s Session) sessionDataFromMap(key string) (result interface{}, err error) {
+ var mapValue map[string]interface{}
+ uncastMapValue, found := s[SessionMapKeyName]
+ if !found {
+ mapValue = map[string]interface{}{}
+ s[SessionMapKeyName] = mapValue
+ } else if mapValue, found = uncastMapValue.(map[string]interface{}); !found {
+ // Unusual means that the value in the session was not expected
+ sessionLog.Errorf("Unusual means that the value in the session was not expected", "session", uncastMapValue)
+ mapValue = map[string]interface{}{}
+ s[SessionMapKeyName] = mapValue
+ }
+
+ // Try to extract the key from the map
+ result, found = mapValue[key]
+ if !found {
+ result, err = s.convertSessionData(key, nil)
+ if err == nil {
+ mapValue[key] = result
+ }
+ }
+ return
+}
+
+// Unpack the object from the session map and store it in the session when done, if no error occurs
+func (s Session) sessionDataFromObject(key string, newValue interface{}) (result interface{}, err error) {
+ result, err = s.convertSessionData(key, newValue)
+ if err != nil {
+ return
+ }
+ s[key] = result
+ return
+}
+
+// Converts from the session json map into the target,
+func (s Session) convertSessionData(key string, target interface{}) (result interface{}, err error) {
+ sessionJsonMap := s.getSessionJsonMap()
+ v, found := sessionJsonMap[key]
+ if !found {
+ return target, SESSION_VALUE_NOT_FOUND
+ }
+
+ // Create a target if needed
+ if target == nil {
+ target = map[string]interface{}{}
+ if err := json.Unmarshal([]byte(v), &target); err != nil {
+ return target, err
+ }
+ } else if err := json.Unmarshal([]byte(v), target); err != nil {
+ return target, err
+ }
+ result = target
+ return
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session_test
+
+import (
+ "testing"
+
+ "github.com/revel/revel"
+ "github.com/revel/revel/session"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "time"
+)
+
+func TestCookieRestore(t *testing.T) {
+ a := assert.New(t)
+ session.InitSession(revel.RevelLog)
+
+ cse := revel.NewSessionCookieEngine()
+ originSession := session.NewSession()
+ setSharedDataTest(originSession)
+ originSession["foo"] = "foo"
+ originSession["bar"] = "bar"
+ cookie := cse.GetCookie(originSession)
+ if !cookie.Expires.IsZero() {
+ t.Error("incorrect cookie expire", cookie.Expires)
+ }
+
+ restoredSession := session.NewSession()
+ cse.DecodeCookie(revel.GoCookie(*cookie), restoredSession)
+ a.Equal("foo",restoredSession["foo"])
+ a.Equal("bar",restoredSession["bar"])
+ testSharedData(originSession, restoredSession, t, a)
+}
+
+func TestCookieSessionExpire(t *testing.T) {
+ session.InitSession(revel.RevelLog)
+ cse := revel.NewSessionCookieEngine()
+ cse.ExpireAfterDuration = time.Hour
+ session := session.NewSession()
+ session["user"] = "Tom"
+ var cookie *http.Cookie
+ for i := 0; i < 3; i++ {
+ cookie = cse.GetCookie(session)
+ time.Sleep(time.Second)
+
+ cse.DecodeCookie(revel.GoCookie(*cookie), session)
+ }
+ expectExpire := time.Now().Add(cse.ExpireAfterDuration)
+ if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() {
+ t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second))
+ }
+ if cookie.Expires.Unix() > expectExpire.Unix() {
+ t.Error("expect expires", cookie.Expires, "before", expectExpire)
+ }
+
+ // Test that the expiration time is zero for a "browser" session
+ session.SetNoExpiration()
+ cookie = cse.GetCookie(session)
+ if !cookie.Expires.IsZero() {
+ t.Error("expect cookie expires is zero")
+ }
+
+ // Check the default session is set
+ session.SetDefaultExpiration()
+ cookie = cse.GetCookie(session)
+ expectExpire = time.Now().Add(cse.ExpireAfterDuration)
+ if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() {
+ t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second))
+ }
+ if cookie.Expires.Unix() > expectExpire.Unix() {
+ t.Error("expect expires", cookie.Expires, "before", expectExpire)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session_test
+
+import (
+ "fmt"
+ "github.com/revel/revel"
+ "github.com/revel/revel/session"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+// test the commands
+func TestSessionString(t *testing.T) {
+ session.InitSession(revel.RevelLog)
+ a := assert.New(t)
+ s := session.NewSession()
+ s.Set("happy", "day")
+ a.Equal("day", s.GetDefault("happy", nil, ""), fmt.Sprintf("Session Data %#v\n", s))
+
+}
+
+func TestSessionStruct(t *testing.T) {
+ session.InitSession(revel.RevelLog)
+ a := assert.New(t)
+ s := session.NewSession()
+ setSharedDataTest(s)
+ a.Equal("test", s.GetDefault("happy.a.aa", nil, ""), fmt.Sprintf("Session Data %#v\n", s))
+
+ stringMap := s.Serialize()
+ s1 := session.NewSession()
+ s1.Load(stringMap)
+ testSharedData(s, s1, t, a)
+
+}
+
+func setSharedDataTest(s session.Session) {
+ data := struct {
+ A struct {
+ Aa string
+ }
+ B int
+ C string
+ D float32
+ }{A: struct {
+ Aa string
+ }{Aa: "test"},
+ B: 5,
+ C: "test",
+ D: -325.25}
+ s.Set("happy", data)
+}
+func testSharedData(s, s1 session.Session, t *testing.T, a *assert.Assertions) {
+ // Compress the session to a string
+ t.Logf("Original session %#v\n", s)
+ t.Logf("New built session %#v\n", s1)
+ data,err := s1.Get("happy.a.aa")
+ a.Nil(err,"Expected nil")
+ a.Equal("test", data, fmt.Sprintf("Session Data %#v\n", s))
+ t.Logf("After test session %#v\n", s1)
+
+}
--- /dev/null
+package revel
+
+import (
+ "fmt"
+ "github.com/revel/revel/session"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+
+type (
+ // The session cookie engine
+ SessionCookieEngine struct {
+ ExpireAfterDuration time.Duration
+ }
+)
+
+// A logger for the session engine
+var sessionEngineLog = RevelLog.New("section", "session-engine")
+
+// Create a new instance to test
+func init() {
+ RegisterSessionEngine(initCookieEngine, "revel-cookie")
+}
+
+// For testing purposes this engine is used
+func NewSessionCookieEngine() *SessionCookieEngine {
+ ce := &SessionCookieEngine{}
+ return ce
+}
+
+// Called when the the application starts, retrieves data from the app config so cannot be run until then
+func initCookieEngine() SessionEngine {
+ ce := &SessionCookieEngine{}
+
+ var err error
+ if expiresString, ok := Config.String("session.expires"); !ok {
+ ce.ExpireAfterDuration = 30 * 24 * time.Hour
+ } else if expiresString == session.SessionValueName {
+ ce.ExpireAfterDuration = 0
+ } else if ce.ExpireAfterDuration, err = time.ParseDuration(expiresString); err != nil {
+ panic(fmt.Errorf("session.expires invalid: %s", err))
+ }
+
+ return ce
+}
+
+// Decode the session information from the cookie retrieved from the controller request
+func (cse *SessionCookieEngine) Decode(c *Controller) {
+ // Decode the session from a cookie.
+ c.Session = map[string]interface{}{}
+ sessionMap := c.Session
+ if cookie, err := c.Request.Cookie(CookiePrefix + session.SessionCookieSuffix); err != nil {
+ return
+ } else {
+ cse.DecodeCookie(cookie, sessionMap)
+ c.Session = sessionMap
+ }
+}
+
+// Encode the session information to the cookie, set the cookie on the controller
+func (cse *SessionCookieEngine) Encode(c *Controller) {
+
+ c.SetCookie(cse.GetCookie(c.Session))
+}
+
+// Exposed only for testing purposes
+func (cse *SessionCookieEngine) DecodeCookie(cookie ServerCookie, s session.Session) {
+ // Decode the session from a cookie.
+ // Separate the data from the signature.
+ cookieValue := cookie.GetValue()
+ hyphen := strings.Index(cookieValue, "-")
+ if hyphen == -1 || hyphen >= len(cookieValue)-1 {
+ return
+ }
+ sig, data := cookieValue[:hyphen], cookieValue[hyphen+1:]
+
+ // Verify the signature.
+ if !Verify(data, sig) {
+ sessionEngineLog.Warn("Session cookie signature failed")
+ return
+ }
+
+ // Parse the cookie into a temp map, and then load it into the session object
+ tempMap := map[string]string{}
+ ParseKeyValueCookie(data, func(key, val string) {
+ tempMap[key] = val
+ })
+ s.Load(tempMap)
+
+ // Check timeout after unpacking values - if timeout missing (or removed) destroy all session
+ // objects
+ if s.SessionTimeoutExpiredOrMissing() {
+ // If this fails we need to delete all the keys from the session
+ for key := range s {
+ delete(s, key)
+ }
+ }
+}
+
+// Convert session to cookie
+func (cse *SessionCookieEngine) GetCookie(s session.Session) *http.Cookie {
+ var sessionValue string
+ ts := s.GetExpiration(cse.ExpireAfterDuration)
+ if ts.IsZero() {
+ s[session.TimestampKey] = session.SessionValueName
+ } else {
+ s[session.TimestampKey] = strconv.FormatInt(ts.Unix(), 10)
+ }
+
+ // Convert the key to a string map
+ stringMap := s.Serialize()
+
+ for key, value := range stringMap {
+ if strings.ContainsAny(key, ":\x00") {
+ panic("Session keys may not have colons or null bytes")
+ }
+ if strings.Contains(value, "\x00") {
+ panic("Session values may not have null bytes")
+ }
+ sessionValue += "\x00" + key + ":" + value + "\x00"
+ }
+
+ if len(sessionValue) > 1024*4 {
+ sessionEngineLog.Error("SessionCookieEngine.Cookie, session data has exceeded 4k limit (%d) cookie data will not be reliable", "length", len(sessionValue))
+ }
+
+ sessionData := url.QueryEscape(sessionValue)
+ sessionCookie := &http.Cookie{
+ Name: CookiePrefix + session.SessionCookieSuffix,
+ Value: Sign(sessionData) + "-" + sessionData,
+ Domain: CookieDomain,
+ Path: "/",
+ HttpOnly: true,
+ Secure: CookieSecure,
+ Expires: ts.UTC(),
+ MaxAge: int(cse.ExpireAfterDuration.Seconds()),
+ }
+ return sessionCookie
+
+}
--- /dev/null
+package revel
+
+// The session engine provides an interface to allow for storage of session data
+type (
+ SessionEngine interface {
+ Decode(c *Controller) // Called to decode the session information on the controller
+ Encode(c *Controller) // Called to encode the session information on the controller
+ }
+)
+
+var (
+ sessionEngineMap = map[string]func() SessionEngine{}
+ CurrentSessionEngine SessionEngine
+)
+
+// Initialize session engine on startup
+func init() {
+ OnAppStart(initSessionEngine, 5)
+}
+
+func RegisterSessionEngine(f func() SessionEngine, name string) {
+ sessionEngineMap[name] = f
+}
+
+// Called when application is starting up
+func initSessionEngine() {
+ // Check for session engine to use and assign it
+ sename := Config.StringDefault("session.engine", "revel-cookie")
+ if se, found := sessionEngineMap[sename]; found {
+ CurrentSessionEngine = se()
+ } else {
+ sessionLog.Warn("Session engine '%s' not found, using default session engine revel-cookie", sename)
+ CurrentSessionEngine = sessionEngineMap["revel-cookie"]()
+ }
+}
--- /dev/null
+package revel
+
+// SessionFilter is a Revel Filter that retrieves and sets the session cookie.
+// Within Revel, it is available as a Session attribute on Controller instances.
+// The name of the Session cookie is set as CookiePrefix + "_SESSION".
+import ()
+
+var sessionLog = RevelLog.New("section", "session")
+
+func SessionFilter(c *Controller, fc []Filter) {
+ CurrentSessionEngine.Decode(c)
+ sessionWasEmpty := c.Session.Empty()
+
+ // Make session vars available in templates as {{.session.xyz}}
+ c.ViewArgs["session"] = c.Session
+ c.ViewArgs["_controller"] = c
+
+ fc[0](c, fc[1:])
+
+ // If session is not empty or if session was not empty then
+ // pass it back to the session engine to be encoded
+ if !c.Session.Empty() || !sessionWasEmpty {
+ CurrentSessionEngine.Encode(c)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+// ErrorCSSClass httml CSS error class name
+var ErrorCSSClass = "hasError"
+
+// TemplateLoader object handles loading and parsing of templates.
+// Everything below the application's views directory is treated as a template.
+type TemplateLoader struct {
+ // Paths to search for templates, in priority order.
+ paths []string
+ // load version seed for templates
+ loadVersionSeed int
+ // A templateRuntime of looked up template results
+ runtimeLoader atomic.Value
+ // Lock to prevent concurrent map writes
+ templateMutex sync.Mutex
+}
+
+type Template interface {
+ // The name of the template.
+ Name() string // Name of template
+ // The content of the template as a string (Used in error handling).
+ Content() []string // Content
+ // Called by the server to render the template out the io.Writer, context contains the view args to be passed to the template.
+ Render(wr io.Writer, context interface{}) error
+ // The full path to the file on the disk.
+ Location() string // Disk location
+}
+
+var invalidSlugPattern = regexp.MustCompile(`[^a-z0-9 _-]`)
+var whiteSpacePattern = regexp.MustCompile(`\s+`)
+var templateLog = RevelLog.New("section", "template")
+
+// TemplateOutputArgs returns the result of the template rendered using the passed in arguments.
+func TemplateOutputArgs(templatePath string, args map[string]interface{}) (data []byte, err error) {
+ // Get the Template.
+ lang, _ := args[CurrentLocaleViewArg].(string)
+ template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+ if err != nil {
+ return nil, err
+ }
+ tr := &RenderTemplateResult{
+ Template: template,
+ ViewArgs: args,
+ }
+ b, err := tr.ToBytes()
+ if err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+}
+
+func NewTemplateLoader(paths []string) *TemplateLoader {
+ loader := &TemplateLoader{
+ paths: paths,
+ }
+ return loader
+}
+
+// WatchDir returns true of directory doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool {
+ // Watch all directories, except the ones starting with a dot.
+ return !strings.HasPrefix(info.Name(), ".")
+}
+
+// WatchFile returns true of file doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchFile(basename string) bool {
+ // Watch all files, except the ones starting with a dot.
+ return !strings.HasPrefix(basename, ".")
+}
+
+// DEPRECATED Use TemplateLang, will be removed in future release
+func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) {
+ runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+ return runtimeLoader.TemplateLang(name, "")
+}
+
+func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) {
+ runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+ return runtimeLoader.TemplateLang(name, lang)
+}
+
+// Refresh method scans the views directory and parses all templates as Go Templates.
+// If a template fails to parse, the error is set on the loader.
+// (It's awkward to refresh a single Go Template)
+func (loader *TemplateLoader) Refresh() (err *Error) {
+ loader.templateMutex.Lock()
+ defer loader.templateMutex.Unlock()
+
+ loader.loadVersionSeed++
+ runtimeLoader := &templateRuntime{loader: loader,
+ version: loader.loadVersionSeed,
+ templateMap: map[string]Template{}}
+
+ templateLog.Debug("Refresh: Refreshing templates from ", "path", loader.paths)
+ if err = loader.initializeEngines(runtimeLoader, Config.StringDefault("template.engines", GO_TEMPLATE)); err != nil {
+ return
+ }
+ for _, engine := range runtimeLoader.templatesAndEngineList {
+ engine.Event(TEMPLATE_REFRESH_REQUESTED, nil)
+ }
+ RaiseEvent(TEMPLATE_REFRESH_REQUESTED, nil)
+ defer func() {
+ for _, engine := range runtimeLoader.templatesAndEngineList {
+ engine.Event(TEMPLATE_REFRESH_COMPLETED, nil)
+ }
+ RaiseEvent(TEMPLATE_REFRESH_COMPLETED, nil)
+
+ // Reset the runtimeLoader
+ loader.runtimeLoader.Store(runtimeLoader)
+ }()
+
+ // Resort the paths, make sure the revel path is the last path,
+ // so anything can override it
+ revelTemplatePath := filepath.Join(RevelPath, "templates")
+ // Go through the paths
+ for i, o := range loader.paths {
+ if o == revelTemplatePath && i != len(loader.paths)-1 {
+ loader.paths[i] = loader.paths[len(loader.paths)-1]
+ loader.paths[len(loader.paths)-1] = revelTemplatePath
+ }
+ }
+ templateLog.Debug("Refresh: Refreshing templates from", "path", loader.paths)
+
+ runtimeLoader.compileError = nil
+ runtimeLoader.TemplatePaths = map[string]string{}
+
+ for _, basePath := range loader.paths {
+ // Walk only returns an error if the template loader is completely unusable
+ // (namely, if one of the TemplateFuncs does not have an acceptable signature).
+
+ // Handling symlinked directories
+ var fullSrcDir string
+ f, err := os.Lstat(basePath)
+ if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
+ fullSrcDir, err = filepath.EvalSymlinks(basePath)
+ if err != nil {
+ templateLog.Panic("Refresh: Eval symlinks error ", "error", err)
+ }
+ } else {
+ fullSrcDir = basePath
+ }
+
+ var templateWalker filepath.WalkFunc
+
+ templateWalker = func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ templateLog.Error("Refresh: error walking templates:", "error", err)
+ return nil
+ }
+
+ // Walk into watchable directories
+ if info.IsDir() {
+ if !loader.WatchDir(info) {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // Only add watchable
+ if !loader.WatchFile(info.Name()) {
+ return nil
+ }
+
+ fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath)
+ if err != nil {
+ // Add in this template name to the list of templates unable to be compiled
+ runtimeLoader.compileErrorNameList = append(runtimeLoader.compileErrorNameList, filepath.ToSlash(path[len(fullSrcDir)+1:]))
+ }
+ // Store / report the first error encountered.
+ if err != nil && runtimeLoader.compileError == nil {
+ runtimeLoader.compileError, _ = err.(*Error)
+
+ if nil == runtimeLoader.compileError {
+ _, line, description := ParseTemplateError(err)
+
+ runtimeLoader.compileError = &Error{
+ Title: "Template Compilation Error",
+ Path: path,
+ Description: description,
+ Line: line,
+ SourceLines: strings.Split(string(fileBytes), "\n"),
+ }
+ }
+ templateLog.Errorf("Refresh: Template compilation error (In %s around line %d):\n\t%s",
+ path, runtimeLoader.compileError.Line, err.Error())
+ } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") {
+
+ if compileError, ok := err.(*Error); ok {
+ templateLog.Errorf("Template compilation error (In %s around line %d):\n\t%s",
+ path, compileError.Line, err.Error())
+ } else {
+ templateLog.Errorf("Template compilation error (In %s ):\n\t%s",
+ path, err.Error())
+ }
+ }
+ return nil
+ }
+
+ if _, err = os.Lstat(fullSrcDir); os.IsNotExist(err) {
+ // #1058 Given views/template path is not exists
+ // so no need to walk, move on to next path
+ continue
+ }
+
+ funcErr := Walk(fullSrcDir, templateWalker)
+
+ // If there was an error with the Funcs, set it and return immediately.
+ if funcErr != nil {
+ runtimeLoader.compileError = NewErrorFromPanic(funcErr)
+ return runtimeLoader.compileError
+ }
+ }
+
+ // Note: compileError may or may not be set.
+ return runtimeLoader.compileError
+}
+
+type templateRuntime struct {
+ loader *TemplateLoader
+ // load version for templates
+ version int
+ // Template data and implementation
+ templatesAndEngineList []TemplateEngine
+ // If an error was encountered parsing the templates, it is stored here.
+ compileError *Error
+ // A list of the names of the templates with errors
+ compileErrorNameList []string
+ // Map from template name to the path from whence it was loaded.
+ TemplatePaths map[string]string
+ // A map of looked up template results
+ templateMap map[string]Template
+}
+
+// Checks to see if template exists in templatePaths, if so it is skipped (templates are imported in order
+// reads the template file into memory, replaces namespace keys with module (if found
+func (runtimeLoader *templateRuntime) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) {
+ templateName := filepath.ToSlash(path[len(fullSrcDir)+1:])
+ // Convert template names to use forward slashes, even on Windows.
+ if os.PathSeparator == '\\' {
+ templateName = strings.Replace(templateName, `\`, `/`, -1) // `
+ }
+
+ // Check to see if template was found
+ if place, found := runtimeLoader.TemplatePaths[templateName]; found {
+ templateLog.Debug("findAndAddTemplate: Not Loading, template is already exists: ", "name", templateName, "old",
+ place, "new", path)
+ return
+ }
+
+ fileBytes, err = ioutil.ReadFile(path)
+ if err != nil {
+ templateLog.Error("findAndAddTemplate: Failed reading file:", "path", path, "error", err)
+ return
+ }
+ // Parse template file and replace the "_LOCAL_|" in the template with the module name
+ // allow for namespaces to be renamed "_LOCAL_(.*?)|"
+ if module := ModuleFromPath(path, false); module != nil {
+ fileBytes = namespaceReplace(fileBytes, module)
+ }
+
+ // if we have an engine picked for this template process it now
+ baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes)
+
+ // Try to find a default engine for the file
+ for _, engine := range runtimeLoader.templatesAndEngineList {
+ if engine.Handles(baseTemplate) {
+ _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate)
+ return
+ }
+ }
+
+ // Try all engines available
+ var defaultError error
+ for _, engine := range runtimeLoader.templatesAndEngineList {
+ if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded {
+ return
+ } else {
+ templateLog.Debugf("findAndAddTemplate: Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr.Error())
+ if defaultError == nil {
+ defaultError = loaderr
+ }
+ }
+ }
+
+ // Assign the error from the first parser
+ err = defaultError
+
+ // No engines could be found return the err
+ if err == nil {
+ err = fmt.Errorf("Failed to parse template file using engines %s", path)
+ }
+
+ return
+}
+
+func (runtimeLoader *templateRuntime) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) {
+ if loadedTemplate, found := runtimeLoader.templateMap[baseTemplate.TemplateName]; found {
+ // Duplicate template found in map
+ templateLog.Debug("template already exists in map: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:",
+ loadedTemplate.Location(), "\r\n\tnew file:", baseTemplate.FilePath)
+ return
+ }
+
+ if loadedTemplate := engine.Lookup(baseTemplate.TemplateName); loadedTemplate != nil {
+ // Duplicate template found for engine
+ templateLog.Debug("loadIntoEngine: template already exists: ", "template", baseTemplate.TemplateName, "inengine ", engine.Name(), "old",
+ loadedTemplate.Location(), "new", baseTemplate.FilePath)
+ loaded = true
+ return
+ }
+ if err = engine.ParseAndAdd(baseTemplate); err == nil {
+ if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil {
+ runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl
+ }
+ runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath
+ templateLog.Debugf("loadIntoEngine:Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath)
+ loaded = true
+ } else {
+ templateLog.Debug("loadIntoEngine: Engine failed to compile", "engine", engine.Name(), "file", baseTemplate.FilePath, "error", err)
+ }
+ return
+}
+
+// Parse the line, and description from an error message like:
+// html/template:Application/Register.html:36: no such template "footer.html"
+func ParseTemplateError(err error) (templateName string, line int, description string) {
+ if e, ok := err.(*Error); ok {
+ return "", e.Line, e.Description
+ }
+
+ description = err.Error()
+ i := regexp.MustCompile(`:\d+:`).FindStringIndex(description)
+ if i != nil {
+ line, err = strconv.Atoi(description[i[0]+1 : i[1]-1])
+ if err != nil {
+ templateLog.Error("ParseTemplateError: Failed to parse line number from error message:", "error", err)
+ }
+ templateName = description[:i[0]]
+ if colon := strings.Index(templateName, ":"); colon != -1 {
+ templateName = templateName[colon+1:]
+ }
+ templateName = strings.TrimSpace(templateName)
+ description = description[i[1]+1:]
+ }
+ return templateName, line, description
+}
+
+// Template returns the Template with the given name. The name is the template's path
+// relative to a template loader root.
+//
+// An Error is returned if there was any problem with any of the templates. (In
+// this case, if a template is returned, it may still be usable.)
+func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) {
+ if runtimeLoader.compileError != nil {
+ for _, errName := range runtimeLoader.compileErrorNameList {
+ if name == errName {
+ return nil, runtimeLoader.compileError
+ }
+ }
+ }
+
+ // Fetch the template from the map
+ tmpl = runtimeLoader.templateLoad(name, lang)
+ if tmpl == nil {
+ err = fmt.Errorf("Template %s not found.", name)
+ }
+
+ return
+}
+
+// Load and also updates map if name is not found (to speed up next lookup)
+func (runtimeLoader *templateRuntime) templateLoad(name, lang string) (tmpl Template) {
+ langName := name
+ found := false
+ if lang != "" {
+ // Look up and return the template.
+ langName = name + "." + lang
+ tmpl, found = runtimeLoader.templateMap[langName]
+ if found {
+ return
+ }
+ tmpl, found = runtimeLoader.templateMap[name]
+ } else {
+ tmpl, found = runtimeLoader.templateMap[name]
+ if found {
+ return
+ }
+ }
+
+ if !found {
+ // Neither name is found
+ // Look up and return the template.
+ for _, engine := range runtimeLoader.templatesAndEngineList {
+ if tmpl = engine.Lookup(langName); tmpl != nil {
+ found = true
+ break
+ }
+ if tmpl = engine.Lookup(name); tmpl != nil {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return
+ }
+ }
+
+ // If we found anything store it in the map, we need to copy so we do not
+ // run into concurrency issues
+ runtimeLoader.loader.templateMutex.Lock()
+ defer runtimeLoader.loader.templateMutex.Unlock()
+
+ // In case another thread has loaded the map, reload the atomic value and check
+ newRuntimeLoader := runtimeLoader.loader.runtimeLoader.Load().(*templateRuntime)
+ if newRuntimeLoader.version != runtimeLoader.version {
+ return
+ }
+
+ newTemplateMap := map[string]Template{}
+ for k, v := range newRuntimeLoader.templateMap {
+ newTemplateMap[k] = v
+ }
+ newTemplateMap[langName] = tmpl
+ if _, found := newTemplateMap[name]; !found {
+ newTemplateMap[name] = tmpl
+ }
+ runtimeCopy := &templateRuntime{}
+ *runtimeCopy = *newRuntimeLoader
+ runtimeCopy.templateMap = newTemplateMap
+
+ // Set the atomic value
+ runtimeLoader.loader.runtimeLoader.Store(runtimeCopy)
+ return
+}
+
+func (i *TemplateView) Location() string {
+ return i.FilePath
+}
+
+func (i *TemplateView) Content() (content []string) {
+ if i.FileBytes != nil {
+ // Parse the bytes
+ buffer := bytes.NewBuffer(i.FileBytes)
+ reader := bufio.NewScanner(buffer)
+ for reader.Scan() {
+ content = append(content, string(reader.Bytes()))
+ }
+ }
+
+ return content
+}
+
+func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView {
+ return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath}
+}
--- /dev/null
+package revel
+
+import (
+ "html/template"
+ "io"
+ "log"
+ "strings"
+)
+
+const GO_TEMPLATE = "go"
+
+// Called on startup, initialized when the REVEL_BEFORE_MODULES_LOADED is called
+func init() {
+ AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+ if typeOf == REVEL_BEFORE_MODULES_LOADED {
+ RegisterTemplateLoader(GO_TEMPLATE, func(loader *TemplateLoader) (TemplateEngine, error) {
+ // Set the template delimiters for the project if present, then split into left
+ // and right delimiters around a space character
+
+ TemplateDelims := Config.StringDefault("template.go.delimiters", "")
+ var splitDelims []string
+ if TemplateDelims != "" {
+ splitDelims = strings.Split(TemplateDelims, " ")
+ if len(splitDelims) != 2 {
+ log.Fatalln("app.conf: Incorrect format for template.delimiters")
+ }
+ }
+
+ return &GoEngine{
+ loader: loader,
+ templateSet: template.New("__root__").Funcs(TemplateFuncs),
+ templatesByName: map[string]*GoTemplate{},
+ splitDelims: splitDelims,
+ }, nil
+ })
+ }
+ return
+ })
+}
+
+// Adapter for Go Templates.
+type GoTemplate struct {
+ *template.Template
+ engine *GoEngine
+ *TemplateView
+}
+
+// return a 'revel.Template' from Go's template.
+func (gotmpl GoTemplate) Render(wr io.Writer, arg interface{}) error {
+ return gotmpl.Execute(wr, arg)
+}
+
+// The main template engine for Go
+type GoEngine struct {
+ // The template loader
+ loader *TemplateLoader
+ // THe current template set
+ templateSet *template.Template
+ // A map of templates by name
+ templatesByName map[string]*GoTemplate
+ // The delimiter that is used to indicate template code, defaults to {{
+ splitDelims []string
+ // True if map is case insensitive
+ CaseInsensitive bool
+}
+
+// Convert the path to lower case if needed
+func (i *GoEngine) ConvertPath(path string) string {
+ if i.CaseInsensitive {
+ return strings.ToLower(path)
+ }
+ return path
+}
+
+// Returns true if this engine can handle the response
+func (i *GoEngine) Handles(templateView *TemplateView) bool {
+ return EngineHandles(i, templateView)
+}
+
+// Parses the template vide and adds it to the template set
+func (engine *GoEngine) ParseAndAdd(baseTemplate *TemplateView) error {
+ // If alternate delimiters set for the project, change them for this set
+ if engine.splitDelims != nil && strings.Index(baseTemplate.Location(), ViewsPath) > -1 {
+ engine.templateSet.Delims(engine.splitDelims[0], engine.splitDelims[1])
+ } else {
+ // Reset to default otherwise
+ engine.templateSet.Delims("", "")
+ }
+ templateSource := string(baseTemplate.FileBytes)
+ templateName := engine.ConvertPath(baseTemplate.TemplateName)
+ tpl, err := engine.templateSet.New(baseTemplate.TemplateName).Parse(templateSource)
+ if nil != err {
+ _, line, description := ParseTemplateError(err)
+ return &Error{
+ Title: "Template Compilation Error",
+ Path: baseTemplate.TemplateName,
+ Description: description,
+ Line: line,
+ SourceLines: strings.Split(templateSource, "\n"),
+ }
+ }
+ engine.templatesByName[templateName] = &GoTemplate{Template: tpl, engine: engine, TemplateView: baseTemplate}
+ return nil
+}
+
+// Lookups the template name, to see if it is contained in this engine
+func (engine *GoEngine) Lookup(templateName string) Template {
+ // Case-insensitive matching of template file name
+ if tpl, found := engine.templatesByName[engine.ConvertPath(templateName)]; found {
+ return tpl
+ }
+ return nil
+}
+
+// Return the engine name
+func (engine *GoEngine) Name() string {
+ return GO_TEMPLATE
+}
+
+// An event listener to listen for Revel INIT events
+func (engine *GoEngine) Event(action Event, i interface{}) {
+ if action == TEMPLATE_REFRESH_REQUESTED {
+ // At this point all the templates have been passed into the
+ engine.templatesByName = map[string]*GoTemplate{}
+ engine.templateSet = template.New("__root__").Funcs(TemplateFuncs)
+ // Check to see what should be used for case sensitivity
+ engine.CaseInsensitive = Config.BoolDefault("go.template.caseinsensitive", true)
+ }
+}
--- /dev/null
+package revel
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+type TemplateEngine interface {
+ // prase template string and add template to the set.
+ ParseAndAdd(basePath *TemplateView) error
+
+ // returns Template corresponding to the given templateName, or nil
+ Lookup(templateName string) Template
+
+ // Fired by the template loader when events occur
+ Event(event Event, arg interface{})
+
+ // returns true if this engine should be used to parse the file specified in baseTemplate
+ Handles(templateView *TemplateView) bool
+
+ // returns the name of the engine
+ Name() string
+}
+
+// The template view information
+type TemplateView struct {
+ TemplateName string // The name of the view
+ FilePath string // The file path (view relative)
+ BasePath string // The file system base path
+ FileBytes []byte // The file loaded
+ EngineType string // The name of the engine used to render the view
+}
+
+var templateLoaderMap = map[string]func(loader *TemplateLoader) (TemplateEngine, error){}
+
+// Allow for templates to be registered during init but not initialized until application has been started
+func RegisterTemplateLoader(key string, loader func(loader *TemplateLoader) (TemplateEngine, error)) (err error) {
+ if _, found := templateLoaderMap[key]; found {
+ err = fmt.Errorf("Template loader %s already exists", key)
+ }
+ templateLog.Debug("Registered template engine loaded", "name", key)
+ templateLoaderMap[key] = loader
+ return
+}
+
+// Sets the template name from Config
+// Sets the template API methods for parsing and storing templates before rendering
+func (loader *TemplateLoader) CreateTemplateEngine(templateEngineName string) (TemplateEngine, error) {
+ if "" == templateEngineName {
+ templateEngineName = GO_TEMPLATE
+ }
+ factory := templateLoaderMap[templateEngineName]
+ if nil == factory {
+ fmt.Printf("registered factories %#v\n %s \n", templateLoaderMap, templateEngineName)
+ return nil, errors.New("Unknown template engine name - " + templateEngineName + ".")
+ }
+ templateEngine, err := factory(loader)
+ if nil != err {
+ return nil, errors.New("Failed to init template engine (" + templateEngineName + "), " + err.Error())
+ }
+
+ templateLog.Debug("CreateTemplateEngine: init templates", "name", templateEngineName)
+ return templateEngine, nil
+}
+
+// Passing in a comma delimited list of engine names to be used with this loader to parse the template files
+func (loader *TemplateLoader) initializeEngines(runtimeLoader *templateRuntime, templateEngineNameList string) (err *Error) {
+ // Walk through the template loader's paths and build up a template set.
+ if templateEngineNameList == "" {
+ templateEngineNameList = GO_TEMPLATE
+
+ }
+ runtimeLoader.templatesAndEngineList = []TemplateEngine{}
+ for _, engine := range strings.Split(templateEngineNameList, ",") {
+ engine := strings.TrimSpace(strings.ToLower(engine))
+
+ if templateLoader, err := loader.CreateTemplateEngine(engine); err != nil {
+ runtimeLoader.compileError = &Error{
+ Title: "Panic (Template Loader)",
+ Description: err.Error(),
+ }
+ return runtimeLoader.compileError
+ } else {
+ // Always assign a default engine, switch it if it is specified in the config
+ runtimeLoader.templatesAndEngineList = append(runtimeLoader.templatesAndEngineList, templateLoader)
+ }
+ }
+ return
+}
+
+func EngineHandles(engine TemplateEngine, templateView *TemplateView) bool {
+ if line, _, e := bufio.NewReader(bytes.NewBuffer(templateView.FileBytes)).ReadLine(); e == nil && string(line[:3]) == "#! " {
+ // Extract the shebang and look at the rest of the line
+ // #! pong2
+ // #! go
+ templateType := strings.TrimSpace(string(line[2:]))
+ if engine.Name() == templateType {
+ // Advance the read file bytes so it does not include the shebang
+ templateView.FileBytes = templateView.FileBytes[len(line)+1:]
+ templateView.EngineType = templateType
+ return true
+ }
+ }
+ filename := filepath.Base(templateView.FilePath)
+ bits := strings.Split(filename, ".")
+ if len(bits) > 2 {
+ templateType := strings.TrimSpace(bits[len(bits)-2])
+ if engine.Name() == templateType {
+ templateView.EngineType = templateType
+ return true
+ }
+ }
+ return false
+}
--- /dev/null
+package revel
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "github.com/xeonx/timeago"
+ "html"
+ "html/template"
+ "reflect"
+ "strings"
+ "time"
+)
+
+var (
+ // The functions available for use in the templates.
+ TemplateFuncs = map[string]interface{}{
+ "url": ReverseURL,
+ "set": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
+ viewArgs[key] = value
+ return template.JS("")
+ },
+ "append": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
+ if viewArgs[key] == nil {
+ viewArgs[key] = []interface{}{value}
+ } else {
+ viewArgs[key] = append(viewArgs[key].([]interface{}), value)
+ }
+ return template.JS("")
+ },
+ "field": NewField,
+ "firstof": func(args ...interface{}) interface{} {
+ for _, val := range args {
+ switch val.(type) {
+ case nil:
+ continue
+ case string:
+ if val == "" {
+ continue
+ }
+ return val
+ default:
+ return val
+ }
+ }
+ return nil
+ },
+ "option": func(f *Field, val interface{}, label string) template.HTML {
+ selected := ""
+ if f.Flash() == val || (f.Flash() == "" && f.Value() == val) {
+ selected = " selected"
+ }
+
+ return template.HTML(fmt.Sprintf(`<option value="%s"%s>%s</option>`,
+ html.EscapeString(fmt.Sprintf("%v", val)), selected, html.EscapeString(label)))
+ },
+ "radio": func(f *Field, val string) template.HTML {
+ checked := ""
+ if f.Flash() == val {
+ checked = " checked"
+ }
+ return template.HTML(fmt.Sprintf(`<input type="radio" name="%s" value="%s"%s>`,
+ html.EscapeString(f.Name), html.EscapeString(val), checked))
+ },
+ "checkbox": func(f *Field, val string) template.HTML {
+ checked := ""
+ if f.Flash() == val {
+ checked = " checked"
+ }
+ return template.HTML(fmt.Sprintf(`<input type="checkbox" name="%s" value="%s"%s>`,
+ html.EscapeString(f.Name), html.EscapeString(val), checked))
+ },
+ // Pads the given string with 's up to the given width.
+ "pad": func(str string, width int) template.HTML {
+ if len(str) >= width {
+ return template.HTML(html.EscapeString(str))
+ }
+ return template.HTML(html.EscapeString(str) + strings.Repeat(" ", width-len(str)))
+ },
+
+ "errorClass": func(name string, viewArgs map[string]interface{}) template.HTML {
+ errorMap, ok := viewArgs["errors"].(map[string]*ValidationError)
+ if !ok || errorMap == nil {
+ templateLog.Warn("errorClass: Called 'errorClass' without 'errors' in the view args.")
+ return template.HTML("")
+ }
+ valError, ok := errorMap[name]
+ if !ok || valError == nil {
+ return template.HTML("")
+ }
+ return template.HTML(ErrorCSSClass)
+ },
+
+ "msg": func(viewArgs map[string]interface{}, message string, args ...interface{}) template.HTML {
+ str, ok := viewArgs[CurrentLocaleViewArg].(string)
+ if !ok {
+ return ""
+ }
+ return template.HTML(MessageFunc(str, message, args...))
+ },
+
+ // Replaces newlines with <br>
+ "nl2br": func(text string) template.HTML {
+ return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
+ },
+
+ // Skips sanitation on the parameter. Do not use with dynamic data.
+ "raw": func(text string) template.HTML {
+ return template.HTML(text)
+ },
+
+ // Pluralize, a helper for pluralizing words to correspond to data of dynamic length.
+ // items - a slice of items, or an integer indicating how many items there are.
+ // pluralOverrides - optional arguments specifying the output in the
+ // singular and plural cases. by default "" and "s"
+ "pluralize": func(items interface{}, pluralOverrides ...string) string {
+ singular, plural := "", "s"
+ if len(pluralOverrides) >= 1 {
+ singular = pluralOverrides[0]
+ if len(pluralOverrides) == 2 {
+ plural = pluralOverrides[1]
+ }
+ }
+
+ switch v := reflect.ValueOf(items); v.Kind() {
+ case reflect.Int:
+ if items.(int) != 1 {
+ return plural
+ }
+ case reflect.Slice:
+ if v.Len() != 1 {
+ return plural
+ }
+ default:
+ templateLog.Error("pluralize: unexpected type: ", "value", v)
+ }
+ return singular
+ },
+
+ // Format a date according to the application's default date(time) format.
+ "date": func(date time.Time) string {
+ return date.Format(DateFormat)
+ },
+ "datetime": func(date time.Time) string {
+ return date.Format(DateTimeFormat)
+ },
+ // Fetch an object from the session.
+ "session": func(key string, viewArgs map[string]interface{}) interface{} {
+ if viewArgs != nil {
+ if c, found := viewArgs["_controller"]; found {
+ if v, err := c.(*Controller).Session.Get(key); err == nil {
+ return v
+ } else {
+ templateLog.Errorf("template.session, key %s error %v", key, err)
+ }
+ } else {
+ templateLog.Warnf("template.session, key %s requested without controller", key)
+ }
+ } else {
+ templateLog.Warnf("template.session, key %s requested passing in view args", key)
+ }
+ return ""
+ },
+
+ "slug": Slug,
+ "even": func(a int) bool { return (a % 2) == 0 },
+
+ // Using https://github.com/xeonx/timeago
+ "timeago": TimeAgo,
+ "i18ntemplate": func(args ...interface{}) (template.HTML, error) {
+ templateName, lang := "", ""
+ var viewArgs interface{}
+ switch len(args) {
+ case 0:
+ templateLog.Error("i18ntemplate: No arguments passed to template call")
+ case 1:
+ // Assume only the template name is passed in
+ templateName = args[0].(string)
+ case 2:
+ // Assume template name and viewArgs is passed in
+ templateName = args[0].(string)
+ viewArgs = args[1]
+ // Try to extract language from the view args
+ if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
+ lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
+ }
+ default:
+ // Assume third argument is the region
+ templateName = args[0].(string)
+ viewArgs = args[1]
+ lang, _ = args[2].(string)
+ if len(args) > 3 {
+ templateLog.Error("i18ntemplate: Received more parameters then needed for", "template", templateName)
+ }
+ }
+
+ var buf bytes.Buffer
+ // Get template
+ tmpl, err := MainTemplateLoader.TemplateLang(templateName, lang)
+ if err == nil {
+ err = tmpl.Render(&buf, viewArgs)
+ } else {
+ templateLog.Error("i18ntemplate: Failed to render i18ntemplate ", "name", templateName, "error", err)
+ }
+ return template.HTML(buf.String()), err
+ },
+ }
+)
+
+/////////////////////
+// Template functions
+/////////////////////
+
+// ReverseURL returns a url capable of invoking a given controller method:
+// "Application.ShowApp 123" => "/app/123"
+func ReverseURL(args ...interface{}) (template.URL, error) {
+ if len(args) == 0 {
+ return "", errors.New("no arguments provided to reverse route")
+ }
+
+ action := args[0].(string)
+ if action == "Root" {
+ return template.URL(AppRoot), nil
+ }
+
+ pathData, found := splitActionPath(nil, action, true)
+
+ if !found {
+ return "", fmt.Errorf("reversing '%s', expected 'Controller.Action'", action)
+ }
+
+ // Look up the types.
+
+ if pathData.TypeOfController == nil {
+ return "", fmt.Errorf("Failed reversing %s: controller not found %#v", action, pathData)
+ }
+
+ // Note method name is case insensitive search
+ methodType := pathData.TypeOfController.Method(pathData.MethodName)
+ if methodType == nil {
+ return "", errors.New("revel/controller: In " + action + " failed to find function " + pathData.MethodName)
+ }
+
+ if len(methodType.Args) < len(args)-1 {
+ return "", fmt.Errorf("reversing %s: route defines %d args, but received %d",
+ action, len(methodType.Args), len(args)-1)
+ }
+ // Unbind the arguments.
+ argsByName := make(map[string]string)
+ // Bind any static args first
+ fixedParams := len(pathData.FixedParamsByName)
+
+ for i, argValue := range args[1:] {
+ Unbind(argsByName, methodType.Args[i+fixedParams].Name, argValue)
+ }
+
+ return template.URL(MainRouter.Reverse(args[0].(string), argsByName).URL), nil
+}
+
+func Slug(text string) string {
+ separator := "-"
+ text = strings.ToLower(text)
+ text = invalidSlugPattern.ReplaceAllString(text, "")
+ text = whiteSpacePattern.ReplaceAllString(text, separator)
+ text = strings.Trim(text, separator)
+ return text
+}
+
+var timeAgoLangs = map[string]timeago.Config{}
+
+func TimeAgo(args ...interface{}) string {
+
+ datetime := time.Now()
+ lang := ""
+ var viewArgs interface{}
+ switch len(args) {
+ case 0:
+ templateLog.Error("TimeAgo: No arguments passed to timeago")
+ case 1:
+ // only the time is passed in
+ datetime = args[0].(time.Time)
+ case 2:
+ // time and region is passed in
+ datetime = args[0].(time.Time)
+ switch v := reflect.ValueOf(args[1]); v.Kind() {
+ case reflect.String:
+ // second params type string equals region
+ lang, _ = args[1].(string)
+ case reflect.Map:
+ // second params type map equals viewArgs
+ viewArgs = args[1]
+ if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
+ lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
+ }
+ default:
+ templateLog.Error("TimeAgo: unexpected type: ", "value", v)
+ }
+ default:
+ // Assume third argument is the region
+ datetime = args[0].(time.Time)
+ if reflect.ValueOf(args[1]).Kind() != reflect.Map {
+ templateLog.Error("TimeAgo: unexpected type", "value", args[1])
+ }
+ if reflect.ValueOf(args[2]).Kind() != reflect.String {
+ templateLog.Error("TimeAgo: unexpected type: ", "value", args[2])
+ }
+ viewArgs = args[1]
+ lang, _ = args[2].(string)
+ if len(args) > 3 {
+ templateLog.Error("TimeAgo: Received more parameters then needed for timeago")
+ }
+ }
+ if lang == "" {
+ lang, _ = Config.String(defaultLanguageOption)
+ if lang == "en" {
+ timeAgoLangs[lang] = timeago.English
+ }
+ }
+ _, ok := timeAgoLangs[lang]
+ if !ok {
+ timeAgoLangs[lang] = timeago.Config{
+ PastPrefix: "",
+ PastSuffix: " " + MessageFunc(lang, "ago"),
+ FuturePrefix: MessageFunc(lang, "in") + " ",
+ FutureSuffix: "",
+ Periods: []timeago.FormatPeriod{
+ {time.Second, MessageFunc(lang, "about a second"), MessageFunc(lang, "%d seconds")},
+ {time.Minute, MessageFunc(lang, "about a minute"), MessageFunc(lang, "%d minutes")},
+ {time.Hour, MessageFunc(lang, "about an hour"), MessageFunc(lang, "%d hours")},
+ {timeago.Day, MessageFunc(lang, "one day"), MessageFunc(lang, "%d days")},
+ {timeago.Month, MessageFunc(lang, "one month"), MessageFunc(lang, "%d months")},
+ {timeago.Year, MessageFunc(lang, "one year"), MessageFunc(lang, "%d years")},
+ },
+ Zero: MessageFunc(lang, "about a second"),
+ Max: 73 * time.Hour,
+ DefaultLayout: "2006-01-02",
+ }
+
+ }
+ return timeAgoLangs[lang].Format(datetime)
+}
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Forbidden</title>
+ </head>
+ <body>
+ {{with .Error}}
+ <h1>
+ {{.Title}}
+ </h1>
+ <p>
+ {{.Description}}
+ </p>
+ {{end}}
+ </body>
+</html>
--- /dev/null
+{
+ "title": "{{js .Error.Title}}",
+ "description": "{{js .Error.Description}}"
+}
--- /dev/null
+{{.Error.Title}}
+
+{{.Error.Description}}
--- /dev/null
+<forbidden>{{.Error.Description}}</forbidden>
--- /dev/null
+<style type="text/css">
+ html, body {
+ margin: 0;
+ padding: 0;
+ font-family: Helvetica, Arial, Sans;
+ background: #EEEEEE;
+ }
+ .block {
+ padding: 20px;
+ border-bottom: 1px solid #aaa;
+ }
+ #header h1 {
+ font-weight: normal;
+ font-size: 28px;
+ margin: 0;
+ }
+ #more {
+ color: #666;
+ font-size: 80%;
+ border: none;
+ }
+ #header {
+ background: #FFFFCC;
+ }
+ #header p {
+ color: #333;
+ }
+ #routes {
+ background: #f6f6f6;
+ }
+ #routes h2 {
+ font-weight: normal;
+ font-size: 18px;
+ margin: 0 0 10px 0;
+ }
+ #routes ol {
+
+ }
+ #routes li {
+ font-size: 14px;
+ font-family: monospace;
+ color: #333;
+ }
+</style>
+
+<div id="header" class="block">
+ {{with .Error}}
+ <h1>
+ {{.Title}}
+ </h1>
+ <p>
+ {{.Description}}
+ </p>
+ {{end}}
+</div>
+<div id="routes" class="block">
+ <h2>These routes have been tried, in this order :</h2>
+ <ol>
+ {{range .Router.Routes}}
+ <li>{{pad .Method 10}}{{pad .Path 50}}{{.Action}} {{with .ModuleSource}}(Route Module:{{.Name}}){{end}}</li>
+ {{end}}
+ </ol>
+</div>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Not found</title>
+ </head>
+ <body>
+
+{{if .DevMode}}
+
+{{template "errors/404-dev.html" .}}
+
+{{else}}
+
+ {{with .Error}}
+ <h1>
+ {{.Title}}
+ </h1>
+ <p>
+ {{.Description}}
+ </p>
+ {{end}}
+
+{{end}}
+
+ </body>
+</html>
--- /dev/null
+{
+ "title": "{{js .Error.Title}}",
+ "description": "{{js .Error.Description}}"
+}
--- /dev/null
+{{.Error.Title}}
+
+{{.Error.Description}}
--- /dev/null
+<notfound>{{.Error.Description}}</notfound>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Method not allowed</title>
+ </head>
+ <body>
+ {{with .Error}}
+ <h1>
+ {{.Title}}
+ </h1>
+ <p>
+ {{.Description}}
+ </p>
+ {{end}}
+ </body>
+</html>
--- /dev/null
+{
+ "title": "{{js .Error.Title}}",
+ "description": "{{js .Error.Description}}"
+}
--- /dev/null
+{{.Error.Title}}
+
+{{.Error.Description}}
--- /dev/null
+<method-not-allowed>{{.Error.Description}}</method-not-allowed>
--- /dev/null
+ <style type="text/css">
+ html, body {
+ margin: 0;
+ padding: 0;
+ font-family: Helvetica, Arial, Sans;
+ background: #EEEEEE;
+ }
+ .block {
+ padding: 20px;
+ border-bottom: 1px solid #aaa;
+ }
+ #header h1 {
+ font-weight: normal;
+ font-size: 28px;
+ margin: 0;
+ }
+ #more {
+ color: #666;
+ font-size: 80%;
+ border: none;
+ }
+ #header {
+ background: #fcd2da;
+ }
+ #header p {
+ color: #333;
+ }
+ #source {
+ background: #f6f6f6;
+ }
+ #source h2 {
+ font-weight: normal;
+ font-size: 18px;
+ margin: 0 0 10px 0;
+ }
+ #source .lineNumber {
+ float: left;
+ display: block;
+ width: 40px;
+ text-align: right;
+ margin-right: 10px;
+ font-size: 14px;
+ font-family: monospace;
+ background: #333;
+ color: #fff;
+ }
+ #source .line {
+ clear: both;
+ color: #333;
+ margin-bottom: 1px;
+ }
+ #source pre {
+ font-size: 14px;
+ margin: 0;
+ overflow-x: hidden;
+ }
+ #source .error {
+ color: #c00 !important;
+ }
+ #source .error .lineNumber {
+ background: #c00;
+ }
+ #source a {
+ text-decoration: none;
+ }
+ #source a:hover * {
+ cursor: pointer !important;
+ }
+ #source a:hover pre {
+ background: #FAFFCF !important;
+ }
+ #source em {
+ font-style: normal;
+ text-decoration: underline;
+ font-weight: bold;
+ }
+ #source strong {
+ font-style: normal;
+ font-weight: bold;
+ }
+ #stack {
+ background: #eee;
+ padding:0 1em 1em;
+ }
+ #stack h3 {
+ font-weight: normal;
+ }
+ #stack code {
+ font-family:monospace;
+ white-space: pre;
+ }
+ </style>
+ {{with .Error}}
+ <div id="header" class="block">
+ <h1>{{.Title}}</h1>
+ <p>
+ {{if .SourceType}}
+ The {{.SourceType}} <strong>{{.Path}}</strong> does not compile: <strong>{{.Description}}</strong>
+ {{else}}
+ {{.Description}}
+ {{end}}
+ </p>
+ </div>
+ {{if .Path}}
+ <div id="source" class="block">
+ <h2>In {{.Path}}
+ {{if .Line}}
+ (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}})
+ {{end}}
+ </h2>
+ {{range .ContextSource}}
+ <div class="line {{if .IsError}}error{{end}}">
+ <span class="lineNumber">{{.Line}}:</span>
+ <pre>{{.Source}}</pre>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ {{if .Stack}}
+ <div id="stack">
+ <h3>Call Stack</h3>
+ <code>{{.Stack}}</code>
+ </div>
+ {{end}}
+ {{if .MetaError}}
+ <div id="source" class="block">
+ <h2>Additionally, an error occurred while handling this error.</h2>
+ <div class="line error">
+ {{.MetaError}}
+ </div>
+ </div>
+ {{end}}
+ {{end}}
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Application error</title>
+ </head>
+ <body>
+ {{if .DevMode}}
+ {{template "errors/500-dev.html" .}}
+ {{else}}
+ <h1>Oops, an error occured</h1>
+ <p>
+ This exception has been logged.
+ </p>
+ {{end}}
+ </body>
+</html>
--- /dev/null
+{
+ "title": "{{js .Error.Title}}",
+ "description": "{{js .Error.Description}}"
+}
--- /dev/null
+{{.Error.Title}}
+{{.Error.Description}}
+
+{{if eq .RunMode "dev"}}
+{{with .Error}}
+{{if .Path}}
+----------
+In {{.Path}} {{if .Line}}(around line {{.Line}}){{end}}
+
+{{range .ContextSource}}
+{{if .IsError}}>{{else}} {{end}} {{.Line}}: {{.Source}}{{end}}
+
+{{end}}
+{{end}}
+{{end}}
--- /dev/null
+<error>
+ <title>{{.Error.Title}}</title>
+ <description>{{.Error.Description}}</description>
+</error>
--- /dev/null
+ </div>
+
+ <div id="footer">
+ Created with the <a href="http://github.com/revel/revel">Revel framework</a> and really inspirated from the booking sample application provided by <a href="http://www.playframework.org">play framework</a>, which was really inspired by the booking sample application provided by the <a href="http://seamframework.org/">seam framework</a>.
+ </div>
+
+ </body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <title>{{.title}}</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" type="text/css" media="screen" href="/public/css/main.css">
+ {{range .moreStyles}}
+ <link rel="stylesheet" type="text/css" href="/public/{{.}}">
+ {{end}}
+ <script src="/public/js/jquery-1.3.2.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="/public/js/sessvars.js" type="text/javascript" charset="utf-8"></script>
+ {{range .moreScripts}}
+ <script src="/public/{{.}}" type="text/javascript" charset="utf-8"></script>
+ {{end}}
+ </head>
+ <body>
+
+ <div id="header">
+ <h1>revel framework booking demo</h1>
+ {{if .user}}
+ <div id="options">
+ Connected as {{.user.Username}}
+ |
+ <a href="{{url "Hotels.Index"}}">Search</a>
+ |
+ <a href="{{url "Hotels.Settings"}}">Settings</a>
+ |
+ <a href="{{url "Application.Logout"}}">Logout</a>
+ </div>
+ {{end}}
+ </div>
+
+ <div id="content">
+ {{if .flash.error}}
+ <p class="fError">
+ <strong>{{.flash.error}}</strong>
+ </p>
+ {{end}}
+ {{if .flash.success}}
+ <p class="fSuccess">
+ <strong>{{.flash.success}}</strong>
+ </p>
+ {{end}}
+
--- /dev/null
+{{template "header.html" .}}
+
+<h1>View hotel</h1>
+
+{{with .hotel}}
+<form action="{{url "Hotels.Book" .HotelID}}">
+
+ <p>
+ <strong>Name:</strong> {{.Name}}
+ </p>
+ <p>
+ <strong>Address:</strong> {{.Address}}
+ </p>
+ <p>
+ <strong>City:</strong> {{.City}}
+ </p>
+ <p>
+ <strong>State:</strong> {{.State}}
+ </p>
+ <p>
+ <strong>Zip:</strong> {{.Zip}}
+ </p>
+ <p>
+ <strong>Country:</strong> {{.Country}}
+ </p>
+ <p>
+ <strong>Nightly rate:</strong> {{.Price}}
+ </p>
+
+ <p class="buttons">
+ <input type="submit" value="Book Hotel">
+ <a href="{{url "Hotels.Index"}}">Back to search</a>
+ </p>
+</form>
+{{end}}
+
+{{template "footer.html" .}}
--- /dev/null
+# Application
+app.name=Booking example
+app.secret=secret
+
+# Server
+http.addr=
+http.port=9000
+http.ssl=false
+http.sslcert=
+http.sslkey=
+
+# Logging
+log.trace.output = stderr
+log.info.output = stderr
+log.warn.output = stderr
+log.error.output = stderr
+
+log.trace.prefix = "TRACE "
+log.info.prefix = "INFO "
+log.warn.prefix = "WARN "
+log.error.prefix = "ERROR "
+
+db.import = github.com/mattn/go-sqlite3
+db.driver = sqlite3
+db.spec = :memory:
+
+build.tags=gorp
+
+# module.jobs=github.com/revel/modules/jobs
+module.static=github.com/revel/modules/static
+
+[dev]
+mode.dev=true
+watch=true
+module.testrunner=github.com/revel/modules/testrunner
+
+[prod]
+watch=false
+module.testrunner=
+
+log.trace.output = off
+log.info.output = off
+log.warn.output = stderr
+log.error.output = stderr
--- /dev/null
+# Routes
+# This file defines all application routes (Higher priority routes first)
+# ~~~~
+
+module:testrunner
+
+GET /hotels Hotels.Index
+GET /hotels/:id Hotels.Show
+GET /hotels/:id/booking Hotels.Book
+
+# Map static resources from the /app/public folder to the /public path
+GET /public/*filepath Static.Serve("public")
+GET /favicon.ico Static.Serve("public/img","favicon.png")
+
+# Catch all
+* /:controller/:action :controller.:action
--- /dev/null
+app.name={{ .AppName }}
+app.secret={{ .Secret }}
+http.addr=
+http.port=9000
+cookie.prefix=REVEL
+
+i18n.default_language=en
+i18n.cookie=APP_LANG
+
+[dev]
+results.pretty=true
+results.staging=true
+watch=true
+
+module.testrunner = github.com/revel/modules/testrunner
+module.static=github.com/revel/modules/static
+
+log.trace.output = off
+log.info.output = stderr
+log.warn.output = stderr
+log.error.output = stderr
+
+[prod]
+results.pretty=false
+results.staging=false
+watch=false
+
+module.testrunner =
+
+log.trace.output = off
+log.info.output = off
+log.warn.output = %(app.name)s.log
+log.error.output = %(app.name)s.log
--- /dev/null
+greeting=Hallo
+greeting.name=Rob
+greeting.suffix=, welkom bij Revel!
+
+[NL]
+greeting=Goeiedag
+
+[BE]
+greeting=Hallokes
--- /dev/null
+greeting=Hello
+greeting.name=Rob
+greeting.suffix=, welcome to Revel!
+
+folded=Greeting is '%(greeting)s'
+folded.arguments=%(greeting.name)s is %d years old
+
+arguments.string=My name is %s
+arguments.hex=The number %d in hexadecimal notation would be %x
+arguments.none=No arguments here son
+
+only_exists_in_default=Default
+
+[AU]
+greeting=G'day
+
+[US]
+greeting=Howdy
+
+[GB]
+greeting=All right
\ No newline at end of file
--- /dev/null
+greeting2=Yo!
--- /dev/null
+console.log('Test file');
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package testing
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime"
+ "mime/multipart"
+ "net/http"
+ "net/http/cookiejar"
+ "net/textproto"
+ "net/url"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/revel/revel"
+
+ "github.com/revel/revel/session"
+ "golang.org/x/net/websocket"
+ "net/http/httptest"
+)
+
+type TestSuite struct {
+ Client *http.Client
+ Response *http.Response
+ ResponseBody []byte
+ Session session.Session
+ SessionEngine revel.SessionEngine
+}
+
+type TestRequest struct {
+ *http.Request
+ testSuite *TestSuite
+}
+
+// This is populated by the generated code in the run/run/go file
+var TestSuites []interface{} // Array of structs that embed TestSuite
+
+// NewTestSuite returns an initialized TestSuite ready for use. It is invoked
+// by the test harness to initialize the embedded field in application tests.
+func NewTestSuite() TestSuite {
+ return NewTestSuiteEngine(revel.NewSessionCookieEngine())
+}
+
+// Define a new test suite with a custom session engine
+func NewTestSuiteEngine(engine revel.SessionEngine) TestSuite {
+ jar, _ := cookiejar.New(nil)
+ ts := TestSuite{
+ Client: &http.Client{Jar: jar},
+ Session: session.NewSession(),
+ SessionEngine: engine,
+ }
+
+ return ts
+}
+
+// NewTestRequest returns an initialized *TestRequest. It is used for extending
+// testsuite package making it possibe to define own methods. Example:
+// type MyTestSuite struct {
+// testing.TestSuite
+// }
+//
+// func (t *MyTestSuite) PutFormCustom(...) {
+// req := http.NewRequest(...)
+// ...
+// return t.NewTestRequest(req)
+// }
+func (t *TestSuite) NewTestRequest(req *http.Request) *TestRequest {
+ request := &TestRequest{
+ Request: req,
+ testSuite: t,
+ }
+ return request
+}
+
+// Host returns the address and port of the server, e.g. "127.0.0.1:8557"
+func (t *TestSuite) Host() string {
+ if revel.ServerEngineInit.Address[0] == ':' {
+ return "127.0.0.1" + revel.ServerEngineInit.Address
+ }
+ return revel.ServerEngineInit.Address
+}
+
+// BaseUrl returns the base http/https URL of the server, e.g. "http://127.0.0.1:8557".
+// The scheme is set to https if http.ssl is set to true in the configuration file.
+func (t *TestSuite) BaseUrl() string {
+ if revel.HTTPSsl {
+ return "https://" + t.Host()
+ }
+ return "http://" + t.Host()
+}
+
+// WebSocketUrl returns the base websocket URL of the server, e.g. "ws://127.0.0.1:8557"
+func (t *TestSuite) WebSocketUrl() string {
+ return "ws://" + t.Host()
+}
+
+// Get issues a GET request to the given path and stores the result in Response
+// and ResponseBody.
+func (t *TestSuite) Get(path string) {
+ t.GetCustom(t.BaseUrl() + path).Send()
+}
+
+// GetCustom returns a GET request to the given URI in a form of its wrapper.
+func (t *TestSuite) GetCustom(uri string) *TestRequest {
+ req, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ panic(err)
+ }
+ return t.NewTestRequest(req)
+}
+
+// Delete issues a DELETE request to the given path and stores the result in
+// Response and ResponseBody.
+func (t *TestSuite) Delete(path string) {
+ t.DeleteCustom(t.BaseUrl() + path).Send()
+}
+
+// DeleteCustom returns a DELETE request to the given URI in a form of its
+// wrapper.
+func (t *TestSuite) DeleteCustom(uri string) *TestRequest {
+ req, err := http.NewRequest("DELETE", uri, nil)
+ if err != nil {
+ panic(err)
+ }
+ return t.NewTestRequest(req)
+}
+
+// Put issues a PUT request to the given path, sending the given Content-Type
+// and data, storing the result in Response and ResponseBody. "data" may be nil.
+func (t *TestSuite) Put(path string, contentType string, reader io.Reader) {
+ t.PutCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PutCustom returns a PUT request to the given URI with specified Content-Type
+// and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PutCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+ req, err := http.NewRequest("PUT", uri, reader)
+ if err != nil {
+ panic(err)
+ }
+ req.Header.Set("Content-Type", contentType)
+ return t.NewTestRequest(req)
+}
+
+// PutForm issues a PUT request to the given path as a form put of the given key
+// and values, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PutForm(path string, data url.Values) {
+ t.PutFormCustom(t.BaseUrl()+path, data).Send()
+}
+
+// PutFormCustom returns a PUT request to the given URI as a form put of the
+// given key and values. The request is in a form of TestRequest wrapper.
+func (t *TestSuite) PutFormCustom(uri string, data url.Values) *TestRequest {
+ return t.PutCustom(uri, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+}
+
+// Patch issues a PATCH request to the given path, sending the given
+// Content-Type and data, and stores the result in Response and ResponseBody.
+// "data" may be nil.
+func (t *TestSuite) Patch(path string, contentType string, reader io.Reader) {
+ t.PatchCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PatchCustom returns a PATCH request to the given URI with specified
+// Content-Type and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PatchCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+ req, err := http.NewRequest("PATCH", uri, reader)
+ if err != nil {
+ panic(err)
+ }
+ req.Header.Set("Content-Type", contentType)
+ return t.NewTestRequest(req)
+}
+
+// Post issues a POST request to the given path, sending the given Content-Type
+// and data, storing the result in Response and ResponseBody. "data" may be nil.
+func (t *TestSuite) Post(path string, contentType string, reader io.Reader) {
+ t.PostCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PostCustom returns a POST request to the given URI with specified
+// Content-Type and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PostCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+ req, err := http.NewRequest("POST", uri, reader)
+ if err != nil {
+ panic(err)
+ }
+ req.Header.Set("Content-Type", contentType)
+ return t.NewTestRequest(req)
+}
+
+// PostForm issues a POST request to the given path as a form post of the given
+// key and values, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PostForm(path string, data url.Values) {
+ t.PostFormCustom(t.BaseUrl()+path, data).Send()
+}
+
+// PostFormCustom returns a POST request to the given URI as a form post of the
+// given key and values. The request is in a form of TestRequest wrapper.
+func (t *TestSuite) PostFormCustom(uri string, data url.Values) *TestRequest {
+ return t.PostCustom(uri, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+}
+
+// PostFile issues a multipart request to the given path sending given params
+// and files, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PostFile(path string, params url.Values, filePaths url.Values) {
+ t.PostFileCustom(t.BaseUrl()+path, params, filePaths).Send()
+}
+
+// PostFileCustom returns a multipart request to the given URI in a form of its
+// wrapper with the given params and files.
+func (t *TestSuite) PostFileCustom(uri string, params url.Values, filePaths url.Values) *TestRequest {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+
+ for key, values := range filePaths {
+ for _, value := range values {
+ createFormFile(writer, key, value)
+ }
+ }
+
+ for key, values := range params {
+ for _, value := range values {
+ err := writer.WriteField(key, value)
+ t.AssertEqual(nil, err)
+ }
+ }
+ err := writer.Close()
+ t.AssertEqual(nil, err)
+
+ return t.PostCustom(uri, writer.FormDataContentType(), body)
+}
+
+// Send issues any request and reads the response. If successful, the caller may
+// examine the Response and ResponseBody properties. Session data will be
+// added.
+func (r *TestRequest) Send() {
+ writer := httptest.NewRecorder()
+ context := revel.NewGoContext(nil)
+ context.Request.SetRequest(r.Request)
+ context.Response.SetResponse(writer)
+ controller := revel.NewController(context)
+ controller.Session = r.testSuite.Session
+
+ r.testSuite.SessionEngine.Encode(controller)
+ response := http.Response{Header: writer.Header()}
+ cookies := response.Cookies()
+ for _, c := range cookies {
+ r.AddCookie(c)
+ }
+ r.MakeRequest()
+}
+
+// MakeRequest issues any request and read the response. If successful, the
+// caller may examine the Response and ResponseBody properties. You will need to
+// manage session / cookie data manually
+func (r *TestRequest) MakeRequest() {
+ var err error
+ if r.testSuite.Response, err = r.testSuite.Client.Do(r.Request); err != nil {
+ panic(err)
+ }
+ if r.testSuite.ResponseBody, err = ioutil.ReadAll(r.testSuite.Response.Body); err != nil {
+ panic(err)
+ }
+
+ // Create the controller again to receive the response for processing.
+ context := revel.NewGoContext(nil)
+ // Set the request with the header from the response..
+ newRequest := &http.Request{URL: r.URL, Header: r.testSuite.Response.Header}
+ for _, cookie := range r.testSuite.Client.Jar.Cookies(r.Request.URL) {
+ newRequest.AddCookie(cookie)
+ }
+ context.Request.SetRequest(newRequest)
+ context.Response.SetResponse(httptest.NewRecorder())
+ controller := revel.NewController(context)
+
+ // Decode the session data from the controller and assign it to the session
+ r.testSuite.SessionEngine.Decode(controller)
+ r.testSuite.Session = controller.Session
+}
+
+// WebSocket creates a websocket connection to the given path and returns it
+func (t *TestSuite) WebSocket(path string) *websocket.Conn {
+ origin := t.BaseUrl() + "/"
+ urlPath := t.WebSocketUrl() + path
+ ws, err := websocket.Dial(urlPath, "", origin)
+ if err != nil {
+ panic(err)
+ }
+ return ws
+}
+
+func (t *TestSuite) AssertOk() {
+ t.AssertStatus(http.StatusOK)
+}
+
+func (t *TestSuite) AssertNotFound() {
+ t.AssertStatus(http.StatusNotFound)
+}
+
+func (t *TestSuite) AssertStatus(status int) {
+ if t.Response.StatusCode != status {
+ panic(fmt.Errorf("Status: (expected) %d != %d (actual)", status, t.Response.StatusCode))
+ }
+}
+
+func (t *TestSuite) AssertContentType(contentType string) {
+ t.AssertHeader("Content-Type", contentType)
+}
+
+func (t *TestSuite) AssertHeader(name, value string) {
+ actual := t.Response.Header.Get(name)
+ if actual != value {
+ panic(fmt.Errorf("Header %s: (expected) %s != %s (actual)", name, value, actual))
+ }
+}
+
+func (t *TestSuite) AssertEqual(expected, actual interface{}) {
+ if !revel.Equal(expected, actual) {
+ panic(fmt.Errorf("(expected) %v != %v (actual)", expected, actual))
+ }
+}
+
+func (t *TestSuite) AssertNotEqual(expected, actual interface{}) {
+ if revel.Equal(expected, actual) {
+ panic(fmt.Errorf("(expected) %v == %v (actual)", expected, actual))
+ }
+}
+
+func (t *TestSuite) Assert(exp bool) {
+ t.Assertf(exp, "Assertion failed")
+}
+
+func (t *TestSuite) Assertf(exp bool, formatStr string, args ...interface{}) {
+ if !exp {
+ panic(fmt.Errorf(formatStr, args...))
+ }
+}
+
+// AssertContains asserts that the response contains the given string.
+func (t *TestSuite) AssertContains(s string) {
+ if !bytes.Contains(t.ResponseBody, []byte(s)) {
+ panic(fmt.Errorf("Assertion failed. Expected response to contain %s", s))
+ }
+}
+
+// AssertNotContains asserts that the response does not contain the given string.
+func (t *TestSuite) AssertNotContains(s string) {
+ if bytes.Contains(t.ResponseBody, []byte(s)) {
+ panic(fmt.Errorf("Assertion failed. Expected response not to contain %s", s))
+ }
+}
+
+// AssertContainsRegex asserts that the response matches the given regular expression.
+func (t *TestSuite) AssertContainsRegex(regex string) {
+ r := regexp.MustCompile(regex)
+
+ if !r.Match(t.ResponseBody) {
+ panic(fmt.Errorf("Assertion failed. Expected response to match regexp %s", regex))
+ }
+}
+
+func createFormFile(writer *multipart.Writer, fieldname, filename string) {
+ // Try to open the file.
+ file, err := os.Open(filename)
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ // Create a new form-data header with the provided field name and file name.
+ // Determine Content-Type of the file by its extension.
+ h := textproto.MIMEHeader{}
+ h.Set("Content-Disposition", fmt.Sprintf(
+ `form-data; name="%s"; filename="%s"`,
+ escapeQuotes(fieldname),
+ escapeQuotes(filepath.Base(filename)),
+ ))
+ h.Set("Content-Type", "application/octet-stream")
+ if ct := mime.TypeByExtension(filepath.Ext(filename)); ct != "" {
+ h.Set("Content-Type", ct)
+ }
+ part, err := writer.CreatePart(h)
+ if err != nil {
+ panic(err)
+ }
+
+ // Copy the content of the file we have opened not reading the whole
+ // file into memory.
+ _, err = io.Copy(part, file)
+ if err != nil {
+ panic(err)
+ }
+}
+
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
+// This function was borrowed from mime/multipart package.
+func escapeQuotes(s string) string {
+ return quoteEscaper.Replace(s)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package testing
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/revel/revel"
+ "github.com/revel/revel/session"
+)
+
+func TestMisc(t *testing.T) {
+ testSuite := createNewTestSuite(t)
+
+ // test Host value
+ if !strings.EqualFold("127.0.0.1:9001", testSuite.Host()) {
+ t.Error("Incorrect Host value found.")
+ }
+
+ // test BaseUrl
+ if !strings.EqualFold("http://127.0.0.1:9001", testSuite.BaseUrl()) {
+ t.Error("Incorrect BaseUrl http value found.")
+ }
+ revel.HTTPSsl = true
+ if !strings.EqualFold("https://127.0.0.1:9001", testSuite.BaseUrl()) {
+ t.Error("Incorrect BaseUrl https value found.")
+ }
+ revel.HTTPSsl = false
+
+ // test WebSocketUrl
+ if !strings.EqualFold("ws://127.0.0.1:9001", testSuite.WebSocketUrl()) {
+ t.Error("Incorrect WebSocketUrl value found.")
+ }
+
+ testSuite.AssertNotEqual("Yes", "No")
+ testSuite.Assert(true)
+}
+
+func TestGet(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Get("/")
+ testSuite.AssertOk()
+ testSuite.AssertContains("this is testcase homepage")
+ testSuite.AssertNotContains("not exists")
+}
+
+func TestGetNotFound(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Get("/notfound")
+ testSuite.AssertNotFound()
+ // testSuite.AssertContains("this is testcase homepage")
+ // testSuite.AssertNotContains("not exists")
+}
+
+// This test is known to fail
+func TestGetCustom(t *testing.T) {
+ testSuite := createNewTestSuite(t)
+ for x := 0; x < 5; x++ {
+ testSuite.GetCustom("http://httpbin.org/get").Send()
+ if testSuite.Response.StatusCode == http.StatusOK {
+ break
+ }
+ println("Failed request from http://httpbin.org/get", testSuite.Response.StatusCode)
+ }
+
+ testSuite.AssertOk()
+ testSuite.AssertContentType("application/json")
+ testSuite.AssertContains("httpbin.org")
+ testSuite.AssertContainsRegex("gzip|deflate")
+}
+
+func TestDelete(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Delete("/purchases/10001")
+ testSuite.AssertOk()
+}
+
+func TestPut(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Put("/purchases/10002",
+ "application/json",
+ bytes.NewReader([]byte(`{"sku":"163645GHT", "desc":"This is test product"}`)),
+ )
+ testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPutForm(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ data := url.Values{}
+ data.Add("name", "beacon1name")
+ data.Add("value", "beacon1value")
+
+ testSuite.PutForm("/send", data)
+ testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPatch(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Patch("/purchases/10003",
+ "application/json",
+ bytes.NewReader([]byte(`{"desc": "This is test patch for product"}`)),
+ )
+ testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPost(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ testSuite.Post("/login",
+ "application/json",
+ bytes.NewReader([]byte(`{"username":"testuser", "password":"testpass"}`)),
+ )
+ testSuite.AssertOk()
+ testSuite.AssertContains("login successful")
+}
+
+func TestPostForm(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ data := url.Values{}
+ data.Add("username", "testuser")
+ data.Add("password", "testpassword")
+
+ testSuite.PostForm("/login", data)
+ testSuite.AssertOk()
+ testSuite.AssertContains("login successful")
+}
+
+func TestPostFileUpload(t *testing.T) {
+ ts := createTestServer(testHandle)
+ defer ts.Close()
+
+ testSuite := createNewTestSuite(t)
+
+ params := url.Values{}
+ params.Add("first_name", "Jeevanandam")
+ params.Add("last_name", "M.")
+
+ currentDir, _ := os.Getwd()
+ basePath := filepath.Dir(currentDir)
+
+ filePaths := url.Values{}
+ filePaths.Add("revel_file", filepath.Join(basePath, "revel.go"))
+ filePaths.Add("server_file", filepath.Join(basePath, "server.go"))
+ filePaths.Add("readme_file", filepath.Join(basePath, "README.md"))
+
+ testSuite.PostFile("/upload", params, filePaths)
+
+ testSuite.AssertOk()
+ testSuite.AssertContains("File: revel.go")
+ testSuite.AssertContains("File: server.go")
+ testSuite.AssertNotContains("File: not_exists.go")
+ testSuite.AssertEqual("text/plain; charset=utf-8", testSuite.Response.Header.Get("Content-Type"))
+
+}
+
+func createNewTestSuite(t *testing.T) *TestSuite {
+ suite := NewTestSuite()
+
+ if suite.Client == nil || suite.SessionEngine == nil {
+ t.Error("Unable to create a testsuite")
+ }
+
+ return &suite
+}
+
+func testHandle(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "GET" {
+ if r.URL.Path == "/" {
+ _, _ = w.Write([]byte(`this is testcase homepage`))
+ return
+ }
+ }
+
+ if r.Method == "POST" {
+ if r.URL.Path == "/login" {
+ http.SetCookie(w, &http.Cookie{
+ Name: session.SessionCookieSuffix,
+ Value: "This is simple session value",
+ Path: "/",
+ HttpOnly: true,
+ Secure: false,
+ Expires: time.Now().Add(time.Minute * 5).UTC(),
+ })
+
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`))
+ return
+ }
+
+ handleFileUpload(w, r)
+ return
+ }
+
+ if r.Method == "DELETE" {
+ if r.URL.Path == "/purchases/10001" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ }
+
+ if r.Method == "PUT" {
+ if r.URL.Path == "/purchases/10002" {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ if r.URL.Path == "/send" {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ }
+
+ if r.Method == "PATCH" {
+ if r.URL.Path == "/purchases/10003" {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ }
+
+ w.WriteHeader(http.StatusNotFound)
+}
+
+func handleFileUpload(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/upload" {
+ _ = r.ParseMultipartForm(10e6)
+ var buf bytes.Buffer
+ for _, fhdrs := range r.MultipartForm.File {
+ for _, hdr := range fhdrs {
+ dotPos := strings.LastIndex(hdr.Filename, ".")
+ fname := fmt.Sprintf("%s-%v%s", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:])
+ _, _ = buf.WriteString(fmt.Sprintf(
+ "Firstname: %v\nLastname: %v\nFile: %v\nHeader: %v\nUploaded as: %v\n",
+ r.FormValue("first_name"),
+ r.FormValue("last_name"),
+ hdr.Filename,
+ hdr.Header,
+ fname))
+ }
+ }
+
+ _, _ = w.Write(buf.Bytes())
+
+ return
+ }
+}
+
+func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
+ testServer := httptest.NewServer(http.HandlerFunc(fn))
+ revel.ServerEngineInit.Address = testServer.URL[7:]
+ return testServer
+}
+
+func init() {
+ if revel.ServerEngineInit == nil {
+ revel.ServerEngineInit = &revel.EngineInit{
+ Address: ":9001",
+ Network: "http",
+ Port: 9001,
+ }
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strings"
+
+ "github.com/revel/config"
+)
+
+const (
+ // DefaultFileContentType Revel's default response content type
+ DefaultFileContentType = "application/octet-stream"
+)
+
+var (
+ cookieKeyValueParser = regexp.MustCompile("\x00([^:]*):([^\x00]*)\x00")
+ HdrForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
+ HdrRealIP = http.CanonicalHeaderKey("X-Real-Ip")
+ utilLog = RevelLog.New("section", "util")
+ mimeConfig *config.Context
+)
+
+// ExecutableTemplate adds some more methods to the default Template.
+type ExecutableTemplate interface {
+ Execute(io.Writer, interface{}) error
+}
+
+// ExecuteTemplate execute a template and returns the result as a string.
+func ExecuteTemplate(tmpl ExecutableTemplate, data interface{}) string {
+ var b bytes.Buffer
+ if err := tmpl.Execute(&b, data); err != nil {
+ utilLog.Error("ExecuteTemplate: Execute failed", "error", err)
+ }
+ return b.String()
+}
+
+// MustReadLines reads the lines of the given file. Panics in the case of error.
+func MustReadLines(filename string) []string {
+ r, err := ReadLines(filename)
+ if err != nil {
+ panic(err)
+ }
+ return r
+}
+
+// ReadLines reads the lines of the given file. Panics in the case of error.
+func ReadLines(filename string) ([]string, error) {
+ dataBytes, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ return strings.Split(string(dataBytes), "\n"), nil
+}
+
+func ContainsString(list []string, target string) bool {
+ for _, el := range list {
+ if el == target {
+ return true
+ }
+ }
+ return false
+}
+
+// FindMethod returns the reflect.Method, given a Receiver type and Func value.
+func FindMethod(recvType reflect.Type, funcVal reflect.Value) *reflect.Method {
+ // It is not possible to get the name of the method from the Func.
+ // Instead, compare it to each method of the Controller.
+ for i := 0; i < recvType.NumMethod(); i++ {
+ method := recvType.Method(i)
+ if method.Func.Pointer() == funcVal.Pointer() {
+ return &method
+ }
+ }
+ return nil
+}
+
+// ParseKeyValueCookie takes the raw (escaped) cookie value and parses out key values.
+func ParseKeyValueCookie(val string, cb func(key, val string)) {
+ val, _ = url.QueryUnescape(val)
+ if matches := cookieKeyValueParser.FindAllStringSubmatch(val, -1); matches != nil {
+ for _, match := range matches {
+ cb(match[1], match[2])
+ }
+ }
+}
+
+// LoadMimeConfig load mime-types.conf on init.
+func LoadMimeConfig() {
+ var err error
+ mimeConfig, err = config.LoadContext("mime-types.conf", ConfPaths)
+ if err != nil {
+ utilLog.Fatal("Failed to load mime type config:", "error", err)
+ }
+}
+
+// ContentTypeByFilename returns a MIME content type based on the filename's extension.
+// If no appropriate one is found, returns "application/octet-stream" by default.
+// Additionally, specifies the charset as UTF-8 for text/* types.
+func ContentTypeByFilename(filename string) string {
+ dot := strings.LastIndex(filename, ".")
+ if dot == -1 || dot+1 >= len(filename) {
+ return DefaultFileContentType
+ }
+
+ extension := filename[dot+1:]
+ contentType := mimeConfig.StringDefault(extension, "")
+ if contentType == "" {
+ return DefaultFileContentType
+ }
+
+ if strings.HasPrefix(contentType, "text/") {
+ return contentType + "; charset=utf-8"
+ }
+
+ return contentType
+}
+
+// DirExists returns true if the given path exists and is a directory.
+func DirExists(filename string) bool {
+ fileInfo, err := os.Stat(filename)
+ return err == nil && fileInfo.IsDir()
+}
+
+func FirstNonEmpty(strs ...string) string {
+ for _, str := range strs {
+ if len(str) > 0 {
+ return str
+ }
+ }
+ return ""
+}
+
+// Equal is a helper for comparing value equality, following these rules:
+// - Values with equivalent types are compared with reflect.DeepEqual
+// - int, uint, and float values are compared without regard to the type width.
+// for example, Equal(int32(5), int64(5)) == true
+// - strings and byte slices are converted to strings before comparison.
+// - else, return false.
+func Equal(a, b interface{}) bool {
+ if reflect.TypeOf(a) == reflect.TypeOf(b) {
+ return reflect.DeepEqual(a, b)
+ }
+ switch a.(type) {
+ case int, int8, int16, int32, int64:
+ switch b.(type) {
+ case int, int8, int16, int32, int64:
+ return reflect.ValueOf(a).Int() == reflect.ValueOf(b).Int()
+ }
+ case uint, uint8, uint16, uint32, uint64:
+ switch b.(type) {
+ case uint, uint8, uint16, uint32, uint64:
+ return reflect.ValueOf(a).Uint() == reflect.ValueOf(b).Uint()
+ }
+ case float32, float64:
+ switch b.(type) {
+ case float32, float64:
+ return reflect.ValueOf(a).Float() == reflect.ValueOf(b).Float()
+ }
+ case string:
+ switch b.(type) {
+ case []byte:
+ return a.(string) == string(b.([]byte))
+ }
+ case []byte:
+ switch b.(type) {
+ case string:
+ return b.(string) == string(a.([]byte))
+ }
+ }
+ return false
+}
+
+// ClientIP method returns client IP address from HTTP request.
+//
+// Note: Set property "app.behind.proxy" to true only if Revel is running
+// behind proxy like nginx, haproxy, apache, etc. Otherwise
+// you may get inaccurate Client IP address. Revel parses the
+// IP address in the order of X-Forwarded-For, X-Real-IP.
+//
+// By default revel will get http.Request's RemoteAddr
+func ClientIP(r *Request) string {
+ if Config.BoolDefault("app.behind.proxy", false) {
+ // Header X-Forwarded-For
+ if fwdFor := strings.TrimSpace(r.GetHttpHeader(HdrForwardedFor)); fwdFor != "" {
+ index := strings.Index(fwdFor, ",")
+ if index == -1 {
+ return fwdFor
+ }
+ return fwdFor[:index]
+ }
+
+ // Header X-Real-Ip
+ if realIP := strings.TrimSpace(r.GetHttpHeader(HdrRealIP)); realIP != "" {
+ return realIP
+ }
+ }
+
+ if remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+ return remoteAddr
+ }
+
+ return ""
+}
+
+// Walk method extends filepath.Walk to also follow symlinks.
+// Always returns the path of the file or directory.
+func Walk(root string, walkFn filepath.WalkFunc) error {
+ return fsWalk(root, root, walkFn)
+}
+
+// createDir method creates nested directories if not exists
+func createDir(path string) error {
+ if _, err := os.Stat(path); err != nil {
+ if os.IsNotExist(err) {
+ if err = os.MkdirAll(path, 0755); err != nil {
+ return fmt.Errorf("Failed to create directory '%v': %v", path, err)
+ }
+ } else {
+ return fmt.Errorf("Failed to create directory '%v': %v", path, err)
+ }
+ }
+ return nil
+}
+
+func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error {
+ fsWalkFunc := func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ var name string
+ name, err = filepath.Rel(fname, path)
+ if err != nil {
+ return err
+ }
+
+ path = filepath.Join(linkName, name)
+
+ if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink {
+ var symlinkPath string
+ symlinkPath, err = filepath.EvalSymlinks(path)
+ if err != nil {
+ return err
+ }
+
+ // https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392
+ info, err = os.Lstat(symlinkPath)
+
+ if err != nil {
+ return walkFn(path, info, err)
+ }
+
+ if info.IsDir() {
+ return fsWalk(symlinkPath, path, walkFn)
+ }
+ }
+
+ return walkFn(path, info, err)
+ }
+ err := filepath.Walk(fname, fsWalkFunc)
+ return err
+}
+
+func init() {
+ OnAppStart(LoadMimeConfig)
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "path/filepath"
+ "reflect"
+ "testing"
+)
+
+func TestContentTypeByFilename(t *testing.T) {
+ testCases := map[string]string{
+ "xyz.jpg": "image/jpeg",
+ "helloworld.c": "text/x-c; charset=utf-8",
+ "helloworld.": "application/octet-stream",
+ "helloworld": "application/octet-stream",
+ "hello.world.c": "text/x-c; charset=utf-8",
+ }
+ srcPath, _ := findSrcPaths(RevelImportPath)
+ ConfPaths = []string{filepath.Join(
+ srcPath,
+ filepath.FromSlash(RevelImportPath),
+ "conf"),
+ }
+ LoadMimeConfig()
+ for filename, expected := range testCases {
+ actual := ContentTypeByFilename(filename)
+ if actual != expected {
+ t.Errorf("%s: %s, Expected %s", filename, actual, expected)
+ }
+ }
+}
+
+func TestEqual(t *testing.T) {
+ type testStruct struct{}
+ type testStruct2 struct{}
+ i, i2 := 8, 9
+ s, s2 := "@朕µ\n\tüöäß", "@朕µ\n\tüöäss"
+ slice, slice2 := []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5}
+ slice3, slice4 := []int{5, 4, 3, 2, 1}, []int{5, 4, 3, 2, 1}
+
+ tm := map[string][]interface{}{
+ "slices": {slice, slice2},
+ "slices2": {slice3, slice4},
+ "types": {new(testStruct), new(testStruct)},
+ "types2": {new(testStruct2), new(testStruct2)},
+ "ints": {int(i), int8(i), int16(i), int32(i), int64(i)},
+ "ints2": {int(i2), int8(i2), int16(i2), int32(i2), int64(i2)},
+ "uints": {uint(i), uint8(i), uint16(i), uint32(i), uint64(i)},
+ "uints2": {uint(i2), uint8(i2), uint16(i2), uint32(i2), uint64(i2)},
+ "floats": {float32(i), float64(i)},
+ "floats2": {float32(i2), float64(i2)},
+ "strings": {[]byte(s), s},
+ "strings2": {[]byte(s2), s2},
+ }
+
+ testRow := func(row, row2 string, expected bool) {
+ for _, a := range tm[row] {
+ for _, b := range tm[row2] {
+ ok := Equal(a, b)
+ if ok != expected {
+ ak := reflect.TypeOf(a).Kind()
+ bk := reflect.TypeOf(b).Kind()
+ t.Errorf("eq(%s=%v,%s=%v) want %t got %t", ak, a, bk, b, expected, ok)
+ }
+ }
+ }
+ }
+
+ testRow("slices", "slices", true)
+ testRow("slices", "slices2", false)
+ testRow("slices2", "slices", false)
+
+ testRow("types", "types", true)
+ testRow("types2", "types", false)
+ testRow("types", "types2", false)
+
+ testRow("ints", "ints", true)
+ testRow("ints", "ints2", false)
+ testRow("ints2", "ints", false)
+
+ testRow("uints", "uints", true)
+ testRow("uints2", "uints", false)
+ testRow("uints", "uints2", false)
+
+ testRow("floats", "floats", true)
+ testRow("floats2", "floats", false)
+ testRow("floats", "floats2", false)
+
+ testRow("strings", "strings", true)
+ testRow("strings2", "strings", false)
+ testRow("strings", "strings2", false)
+}
--- /dev/null
+package utils
+
+import (
+ "fmt"
+ "sync"
+)
+
+type (
+ SimpleLockStack struct {
+ Current *SimpleLockStackElement
+ Creator func() interface{}
+ len int
+ capacity int
+ active int
+ maxsize int
+ lock sync.Mutex
+ }
+ SimpleLockStackElement struct {
+ Value interface{}
+ Previous *SimpleLockStackElement
+ Next *SimpleLockStackElement
+ }
+ ObjectDestroy interface {
+ Destroy()
+ }
+)
+
+func NewStackLock(startsize, maxsize int, creator func() interface{}) *SimpleLockStack {
+ ss := &SimpleLockStack{lock: sync.Mutex{}, Current: &SimpleLockStackElement{Value: creator()}, Creator: creator, maxsize: maxsize}
+ if startsize > 0 {
+ elements := make([]SimpleLockStackElement, startsize-1)
+ current := ss.Current
+ for i := range elements {
+ e := elements[i]
+ if creator != nil {
+ e.Value = creator()
+ }
+ current.Next = &e
+ e.Previous = current
+ current = &e
+ }
+ ss.capacity, ss.len, ss.active = startsize, startsize, 0
+
+ ss.Current = current
+ }
+ return ss
+}
+func (s *SimpleLockStack) Pop() (value interface{}) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ if s.len == 0 {
+ // Pool is empty, create a new item to return
+ if s.Creator != nil {
+ value = s.Creator()
+ }
+ } else {
+ value = s.Current.Value
+ s.len--
+ if s.Current.Previous != nil {
+ s.Current = s.Current.Previous
+ }
+ }
+ // println("Pop ",value, s.len, s.active, s.capacity, s.Current.Next)
+ s.active++
+ return
+}
+func (s *SimpleLockStack) Push(value interface{}) {
+ if d, ok := value.(ObjectDestroy); ok {
+ d.Destroy()
+ }
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ if s.len == 0 {
+ s.Current.Value = value
+ } else if s.len < s.maxsize {
+ if s.Current.Next == nil {
+ s.Current.Next = &SimpleLockStackElement{Value: value, Previous: s.Current}
+ s.capacity++
+ } else {
+ s.Current.Next.Value = value
+ }
+ s.Current = s.Current.Next
+ } else {
+ // If we exceeded the capacity of stack do not store the created object
+ return
+ }
+ s.len++
+ s.active--
+ //println("Push ",value, s.len, s.active, s.capacity)
+ return
+}
+func (s *SimpleLockStack) Len() int {
+ return s.len
+}
+func (s *SimpleLockStack) Capacity() int {
+ return s.capacity
+}
+func (s *SimpleLockStack) Active() int {
+ return s.active
+}
+func (s *SimpleLockStack) String() string {
+ return fmt.Sprintf("SS: Capacity:%d Active:%d Stored:%d", s.capacity, s.active, s.len)
+}
--- /dev/null
+package utils
+
+import (
+ "testing"
+)
+
+type SimpleStackTest struct {
+ index int
+}
+
+func TestUnique(b *testing.T) {
+ stack := NewStackLock(10, 40, func() interface{} {
+ newone := &SimpleStackTest{}
+ return newone
+ })
+ values := []interface{}{}
+ for x := 0; x < 10; x++ {
+ values = append(values, stack.Pop())
+ }
+ if stack.active != 10 {
+ b.Errorf("Failed to match 10 active %v ", stack.active)
+ }
+ value1 := stack.Pop().(*SimpleStackTest)
+ value1.index = stack.active
+ value2 := stack.Pop().(*SimpleStackTest)
+ value2.index = stack.active
+ value3 := stack.Pop().(*SimpleStackTest)
+ value3.index = stack.active
+
+ if !isDifferent(value1, value2, value3) {
+ b.Errorf("Failed to get unique values")
+ }
+
+ if stack.active != 13 {
+ b.Errorf("Failed to match 13 active %v ", stack.active)
+ }
+
+ for _, v := range values {
+ stack.Push(v)
+ }
+ if stack.len != 10 {
+ b.Errorf("Failed to match 10 len %v ", stack.len)
+ }
+ if stack.capacity != 10 {
+ b.Errorf("Failed to capacity 10 len %v ", stack.capacity)
+ }
+
+ stack.Push(value1)
+ stack.Push(value2)
+ stack.Push(value3)
+ if stack.capacity != 13 {
+ b.Errorf("Failed to capacity 13 len %v ", stack.capacity)
+ }
+
+ value1 = stack.Pop().(*SimpleStackTest)
+ value2 = stack.Pop().(*SimpleStackTest)
+ value3 = stack.Pop().(*SimpleStackTest)
+ println(value1, value2, value3)
+ if !isDifferent(value1, value2, value3) {
+ b.Errorf("Failed to get unique values")
+ }
+
+}
+func TestLimits(b *testing.T) {
+ stack := NewStackLock(10, 20, func() interface{} {
+ newone := &SimpleStackTest{}
+ return newone
+ })
+ values := []interface{}{}
+ for x := 0; x < 50; x++ {
+ values = append(values, stack.Pop())
+ }
+ if stack.active != 50 {
+ b.Errorf("Failed to match 50 active %v ", stack.active)
+ }
+ for _, v := range values {
+ stack.Push(v)
+ }
+ if stack.Capacity() != 20 {
+ b.Errorf("Failed to match 20 capcity %v ", stack.Capacity())
+ }
+
+}
+func isDifferent(values ...*SimpleStackTest) bool {
+ if len(values) == 2 {
+ return values[0] != values[1]
+ }
+ for _, v := range values[1:] {
+ if values[0] == v {
+ return false
+ }
+ }
+ return isDifferent(values[1:]...)
+}
+
+func BenchmarkCreateWrite(b *testing.B) {
+ stack := NewStackLock(0, 40, func() interface{} { return &SimpleStackTest{} })
+ for x := 0; x < b.N; x++ {
+ stack.Push(x)
+ }
+}
+func BenchmarkAllocWrite(b *testing.B) {
+ stack := NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+ for x := 0; x < b.N; x++ {
+ stack.Push(x)
+ }
+}
+func BenchmarkCreate(b *testing.B) {
+ NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+}
+func BenchmarkParrallel(b *testing.B) {
+ stack := NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ for x := 0; x < 50000; x++ {
+ stack.Push(x)
+ }
+ }
+ })
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ for x := 0; x < 50000; x++ {
+ stack.Pop()
+ }
+ }
+ })
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "runtime"
+)
+
+// ValidationError simple struct to store the Message & Key of a validation error
+type ValidationError struct {
+ Message, Key string
+}
+
+// String returns the Message field of the ValidationError struct.
+func (e *ValidationError) String() string {
+ if e == nil {
+ return ""
+ }
+ return e.Message
+}
+
+// Validation context manages data validation and error messages.
+type Validation struct {
+ Errors []*ValidationError
+ Request *Request
+ Translator func(locale, message string, args ...interface{}) string
+ keep bool
+}
+
+// Keep tells revel to set a flash cookie on the client to make the validation
+// errors available for the next request.
+// This is helpful when redirecting the client after the validation failed.
+// It is good practice to always redirect upon a HTTP POST request. Thus
+// one should use this method when HTTP POST validation failed and redirect
+// the user back to the form.
+func (v *Validation) Keep() {
+ v.keep = true
+}
+
+// Clear *all* ValidationErrors
+func (v *Validation) Clear() {
+ v.Errors = []*ValidationError{}
+}
+
+// HasErrors returns true if there are any (ie > 0) errors. False otherwise.
+func (v *Validation) HasErrors() bool {
+ return len(v.Errors) > 0
+}
+
+// ErrorMap returns the errors mapped by key.
+// If there are multiple validation errors associated with a single key, the
+// first one "wins". (Typically the first validation will be the more basic).
+func (v *Validation) ErrorMap() map[string]*ValidationError {
+ m := map[string]*ValidationError{}
+ for _, e := range v.Errors {
+ if _, ok := m[e.Key]; !ok {
+ m[e.Key] = e
+ }
+ }
+ return m
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) Error(message string, args ...interface{}) *ValidationResult {
+ result := v.ValidationResult(false).Message(message, args...)
+ v.Errors = append(v.Errors, result.Error)
+ return result
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) ErrorKey(message string, args ...interface{}) *ValidationResult {
+ result := v.ValidationResult(false).MessageKey(message, args...)
+ v.Errors = append(v.Errors, result.Error)
+ return result
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) ValidationResult(ok bool) *ValidationResult {
+ if ok {
+ return &ValidationResult{Ok: ok}
+ } else {
+ return &ValidationResult{Ok: ok, Error: &ValidationError{}, Locale: v.Request.Locale, Translator: v.Translator}
+ }
+}
+
+// ValidationResult is returned from every validation method.
+// It provides an indication of success, and a pointer to the Error (if any).
+type ValidationResult struct {
+ Error *ValidationError
+ Ok bool
+ Locale string
+ Translator func(locale, message string, args ...interface{}) string
+}
+
+// Key sets the ValidationResult's Error "key" and returns itself for chaining
+func (r *ValidationResult) Key(key string) *ValidationResult {
+ if r.Error != nil {
+ r.Error.Key = key
+ }
+ return r
+}
+
+// Message sets the error message for a ValidationResult. Returns itself to
+// allow chaining. Allows Sprintf() type calling with multiple parameters
+func (r *ValidationResult) Message(message string, args ...interface{}) *ValidationResult {
+ if r.Error != nil {
+ if len(args) == 0 {
+ r.Error.Message = message
+ } else {
+ r.Error.Message = fmt.Sprintf(message, args...)
+ }
+ }
+ return r
+}
+
+// Allow a message key to be passed into the validation result. The Validation has already
+// setup the translator to translate the message key
+func (r *ValidationResult) MessageKey(message string, args ...interface{}) *ValidationResult {
+ if r.Error == nil {
+ return r
+ }
+
+ // If translator found, use that to create the message, otherwise call Message method
+ if r.Translator != nil {
+ r.Error.Message = r.Translator(r.Locale, message, args...)
+ } else {
+ r.Message(message, args...)
+ }
+
+ return r
+}
+
+// Required tests that the argument is non-nil and non-empty (if string or list)
+func (v *Validation) Required(obj interface{}) *ValidationResult {
+ return v.apply(Required{}, obj)
+}
+
+func (v *Validation) Min(n int, min int) *ValidationResult {
+ return v.MinFloat(float64(n), float64(min))
+}
+
+func (v *Validation) MinFloat(n float64, min float64) *ValidationResult {
+ return v.apply(Min{min}, n)
+}
+
+func (v *Validation) Max(n int, max int) *ValidationResult {
+ return v.MaxFloat(float64(n), float64(max))
+}
+
+func (v *Validation) MaxFloat(n float64, max float64) *ValidationResult {
+ return v.apply(Max{max}, n)
+}
+
+func (v *Validation) Range(n, min, max int) *ValidationResult {
+ return v.RangeFloat(float64(n), float64(min), float64(max))
+}
+
+func (v *Validation) RangeFloat(n, min, max float64) *ValidationResult {
+ return v.apply(Range{Min{min}, Max{max}}, n)
+}
+
+func (v *Validation) MinSize(obj interface{}, min int) *ValidationResult {
+ return v.apply(MinSize{min}, obj)
+}
+
+func (v *Validation) MaxSize(obj interface{}, max int) *ValidationResult {
+ return v.apply(MaxSize{max}, obj)
+}
+
+func (v *Validation) Length(obj interface{}, n int) *ValidationResult {
+ return v.apply(Length{n}, obj)
+}
+
+func (v *Validation) Match(str string, regex *regexp.Regexp) *ValidationResult {
+ return v.apply(Match{regex}, str)
+}
+
+func (v *Validation) Email(str string) *ValidationResult {
+ return v.apply(Email{Match{emailPattern}}, str)
+}
+
+func (v *Validation) IPAddr(str string, cktype ...int) *ValidationResult {
+ return v.apply(IPAddr{cktype}, str)
+}
+
+func (v *Validation) MacAddr(str string) *ValidationResult {
+ return v.apply(IPAddr{}, str)
+}
+
+func (v *Validation) Domain(str string) *ValidationResult {
+ return v.apply(Domain{}, str)
+}
+
+func (v *Validation) URL(str string) *ValidationResult {
+ return v.apply(URL{}, str)
+}
+
+func (v *Validation) PureText(str string, m int) *ValidationResult {
+ return v.apply(PureText{m}, str)
+}
+
+func (v *Validation) FilePath(str string, m int) *ValidationResult {
+ return v.apply(FilePath{m}, str)
+}
+
+func (v *Validation) apply(chk Validator, obj interface{}) *ValidationResult {
+ if chk.IsSatisfied(obj) {
+ return v.ValidationResult(true)
+ }
+
+ // Get the default key.
+ var key string
+ if pc, _, line, ok := runtime.Caller(2); ok {
+ f := runtime.FuncForPC(pc)
+ if defaultKeys, ok := DefaultValidationKeys[f.Name()]; ok {
+ key = defaultKeys[line]
+ }
+ } else {
+ utilLog.Error("Validation: Failed to get Caller information to look up Validation key")
+ }
+
+ // Add the error to the validation context.
+ err := &ValidationError{
+ Message: chk.DefaultMessage(),
+ Key: key,
+ }
+ v.Errors = append(v.Errors, err)
+
+ // Also return it in the result.
+ vr := v.ValidationResult(false)
+ vr.Error = err
+ return vr
+}
+
+// Check applies a group of validators to a field, in order, and return the
+// ValidationResult from the first one that fails, or the last one that
+// succeeds.
+func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResult {
+ var result *ValidationResult
+ for _, check := range checks {
+ result = v.apply(check, obj)
+ if !result.Ok {
+ return result
+ }
+ }
+ return result
+}
+
+// ValidationFilter revel Filter function to be hooked into the filter chain.
+func ValidationFilter(c *Controller, fc []Filter) {
+ // If json request, we shall assume json response is intended,
+ // as such no validation cookies should be tied response
+ if c.Params != nil && c.Params.JSON != nil {
+ c.Validation = &Validation{Request: c.Request, Translator: MessageFunc}
+ fc[0](c, fc[1:])
+ } else {
+ errors, err := restoreValidationErrors(c.Request)
+ c.Validation = &Validation{
+ Errors: errors,
+ keep: false,
+ Request: c.Request,
+ Translator: MessageFunc,
+ }
+ hasCookie := (err != http.ErrNoCookie)
+
+ fc[0](c, fc[1:])
+
+ // Add Validation errors to ViewArgs.
+ c.ViewArgs["errors"] = c.Validation.ErrorMap()
+
+ // Store the Validation errors
+ var errorsValue string
+ if c.Validation.keep {
+ for _, err := range c.Validation.Errors {
+ if err.Message != "" {
+ errorsValue += "\x00" + err.Key + ":" + err.Message + "\x00"
+ }
+ }
+ }
+
+ // When there are errors from Validation and Keep() has been called, store the
+ // values in a cookie. If there previously was a cookie but no errors, remove
+ // the cookie.
+ if errorsValue != "" {
+ c.SetCookie(&http.Cookie{
+ Name: CookiePrefix + "_ERRORS",
+ Value: url.QueryEscape(errorsValue),
+ Domain: CookieDomain,
+ Path: "/",
+ HttpOnly: true,
+ Secure: CookieSecure,
+ })
+ } else if hasCookie {
+ c.SetCookie(&http.Cookie{
+ Name: CookiePrefix + "_ERRORS",
+ MaxAge: -1,
+ Domain: CookieDomain,
+ Path: "/",
+ HttpOnly: true,
+ Secure: CookieSecure,
+ })
+ }
+ }
+}
+
+// Restore Validation.Errors from a request.
+func restoreValidationErrors(req *Request) ([]*ValidationError, error) {
+ var (
+ err error
+ cookie ServerCookie
+ errors = make([]*ValidationError, 0, 5)
+ )
+ if cookie, err = req.Cookie(CookiePrefix + "_ERRORS"); err == nil {
+ ParseKeyValueCookie(cookie.GetValue(), func(key, val string) {
+ errors = append(errors, &ValidationError{
+ Key: key,
+ Message: val,
+ })
+ })
+ }
+ return errors, err
+}
+
+// DefaultValidationKeys register default validation keys for all calls to Controller.Validation.Func().
+// Map from (package).func => (line => name of first arg to Validation func)
+// E.g. "myapp/controllers.helper" or "myapp/controllers.(*Application).Action"
+// This is set on initialization in the generated main.go file.
+var DefaultValidationKeys map[string]map[int]string
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+// getRecordedCookie returns the recorded cookie from a ResponseRecorder with
+// the given name. It utilizes the cookie reader found in the standard library.
+func getRecordedCookie(recorder *httptest.ResponseRecorder, name string) (*http.Cookie, error) {
+ r := &http.Response{Header: recorder.HeaderMap}
+ for _, cookie := range r.Cookies() {
+ if cookie.Name == name {
+ return cookie, nil
+ }
+ }
+ return nil, http.ErrNoCookie
+}
+
+// r.Original.URL.String()
+func validationTester(req *Request, fn func(c *Controller)) *httptest.ResponseRecorder {
+ recorder := httptest.NewRecorder()
+ c := NewTestController(recorder, req.In.GetRaw().(*http.Request))
+ c.Request = req
+
+ ValidationFilter(c, []Filter{I18nFilter, func(c *Controller, _ []Filter) {
+ fn(c)
+ }})
+ return recorder
+}
+
+// Test that errors are encoded into the _ERRORS cookie.
+func TestValidationWithError(t *testing.T) {
+ recorder := validationTester(buildEmptyRequest().Request, func(c *Controller) {
+ c.Validation.Required("")
+ if !c.Validation.HasErrors() {
+ t.Fatal("errors should be present")
+ }
+ c.Validation.Keep()
+ })
+
+ if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil {
+ t.Fatal(err)
+ } else if cookie.MaxAge < 0 {
+ t.Fatalf("cookie should not expire")
+ }
+}
+
+// Test that no cookie is sent if errors are found, but Keep() is not called.
+func TestValidationNoKeep(t *testing.T) {
+ recorder := validationTester(buildEmptyRequest().Request, func(c *Controller) {
+ c.Validation.Required("")
+ if !c.Validation.HasErrors() {
+ t.Fatal("errors should not be present")
+ }
+ })
+
+ if _, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != http.ErrNoCookie {
+ t.Fatal(err)
+ }
+}
+
+// Test that a previously set _ERRORS cookie is deleted if no errors are found.
+func TestValidationNoKeepCookiePreviouslySet(t *testing.T) {
+ req := buildRequestWithCookie("REVEL_ERRORS", "invalid").Request
+ recorder := validationTester(req, func(c *Controller) {
+ c.Validation.Required("success")
+ if c.Validation.HasErrors() {
+ t.Fatal("errors should not be present")
+ }
+ })
+
+ if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil {
+ t.Fatal(err)
+ } else if cookie.MaxAge >= 0 {
+ t.Fatalf("cookie should be deleted")
+ }
+}
+
+func TestValidateMessageKey(t *testing.T) {
+ Init("prod", "github.com/revel/revel/testdata", "")
+ loadMessages(testDataPath)
+
+ // Assert that we have the expected number of languages
+ if len(MessageLanguages()) != 2 {
+ t.Fatalf("Expected messages to contain no more or less than 2 languages, instead there are %d languages", len(MessageLanguages()))
+ }
+ req := buildRequestWithAcceptLanguages("nl").Request
+
+ validationTester(req, func(c *Controller) {
+ c.Validation.Required("").MessageKey("greeting")
+ if msg := c.Validation.Errors[0].Message; msg != "Hallo" {
+ t.Errorf("Failed expected message Hallo got %s", msg)
+ }
+
+ if !c.Validation.HasErrors() {
+ t.Fatal("errors should not be present")
+ }
+ })
+
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "errors"
+ "fmt"
+ "html"
+ "net"
+ "net/url"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+)
+
+type Validator interface {
+ IsSatisfied(interface{}) bool
+ DefaultMessage() string
+}
+
+type Required struct{}
+
+func ValidRequired() Required {
+ return Required{}
+}
+
+func (r Required) IsSatisfied(obj interface{}) bool {
+ if obj == nil {
+ return false
+ }
+ switch v := reflect.ValueOf(obj); v.Kind() {
+ case reflect.Array, reflect.Slice, reflect.Map, reflect.String, reflect.Chan:
+ if v.Len() == 0 {
+ return false
+ }
+ case reflect.Ptr:
+ return r.IsSatisfied(reflect.Indirect(v).Interface())
+ }
+ return !reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
+}
+
+func (r Required) DefaultMessage() string {
+ return fmt.Sprintln("Required")
+}
+
+type Min struct {
+ Min float64
+}
+
+func ValidMin(min int) Min {
+ return ValidMinFloat(float64(min))
+}
+
+func ValidMinFloat(min float64) Min {
+ return Min{min}
+}
+
+func (m Min) IsSatisfied(obj interface{}) bool {
+ var (
+ num float64
+ ok bool
+ )
+ switch reflect.TypeOf(obj).Kind() {
+ case reflect.Float64:
+ num, ok = obj.(float64)
+ case reflect.Float32:
+ ok = true
+ num = float64(obj.(float32))
+ case reflect.Int:
+ ok = true
+ num = float64(obj.(int))
+ }
+
+ if ok {
+ return num >= m.Min
+ }
+ return false
+}
+
+func (m Min) DefaultMessage() string {
+ return fmt.Sprintln("Minimum is", m.Min)
+}
+
+type Max struct {
+ Max float64
+}
+
+func ValidMax(max int) Max {
+ return ValidMaxFloat(float64(max))
+}
+
+func ValidMaxFloat(max float64) Max {
+ return Max{max}
+}
+
+func (m Max) IsSatisfied(obj interface{}) bool {
+ var (
+ num float64
+ ok bool
+ )
+ switch reflect.TypeOf(obj).Kind() {
+ case reflect.Float64:
+ num, ok = obj.(float64)
+ case reflect.Float32:
+ ok = true
+ num = float64(obj.(float32))
+ case reflect.Int:
+ ok = true
+ num = float64(obj.(int))
+ }
+
+ if ok {
+ return num <= m.Max
+ }
+ return false
+}
+
+func (m Max) DefaultMessage() string {
+ return fmt.Sprintln("Maximum is", m.Max)
+}
+
+// Range requires an integer to be within Min, Max inclusive.
+type Range struct {
+ Min
+ Max
+}
+
+func ValidRange(min, max int) Range {
+ return ValidRangeFloat(float64(min), float64(max))
+}
+
+func ValidRangeFloat(min, max float64) Range {
+ return Range{Min{min}, Max{max}}
+}
+
+func (r Range) IsSatisfied(obj interface{}) bool {
+ return r.Min.IsSatisfied(obj) && r.Max.IsSatisfied(obj)
+}
+
+func (r Range) DefaultMessage() string {
+ return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max)
+}
+
+// MinSize requires an array or string to be at least a given length.
+type MinSize struct {
+ Min int
+}
+
+func ValidMinSize(min int) MinSize {
+ return MinSize{min}
+}
+
+func (m MinSize) IsSatisfied(obj interface{}) bool {
+ if str, ok := obj.(string); ok {
+ return utf8.RuneCountInString(str) >= m.Min
+ }
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Slice {
+ return v.Len() >= m.Min
+ }
+ return false
+}
+
+func (m MinSize) DefaultMessage() string {
+ return fmt.Sprintln("Minimum size is", m.Min)
+}
+
+// MaxSize requires an array or string to be at most a given length.
+type MaxSize struct {
+ Max int
+}
+
+func ValidMaxSize(max int) MaxSize {
+ return MaxSize{max}
+}
+
+func (m MaxSize) IsSatisfied(obj interface{}) bool {
+ if str, ok := obj.(string); ok {
+ return utf8.RuneCountInString(str) <= m.Max
+ }
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Slice {
+ return v.Len() <= m.Max
+ }
+ return false
+}
+
+func (m MaxSize) DefaultMessage() string {
+ return fmt.Sprintln("Maximum size is", m.Max)
+}
+
+// Length requires an array or string to be exactly a given length.
+type Length struct {
+ N int
+}
+
+func ValidLength(n int) Length {
+ return Length{n}
+}
+
+func (s Length) IsSatisfied(obj interface{}) bool {
+ if str, ok := obj.(string); ok {
+ return utf8.RuneCountInString(str) == s.N
+ }
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Slice {
+ return v.Len() == s.N
+ }
+ return false
+}
+
+func (s Length) DefaultMessage() string {
+ return fmt.Sprintln("Required length is", s.N)
+}
+
+// Match requires a string to match a given regex.
+type Match struct {
+ Regexp *regexp.Regexp
+}
+
+func ValidMatch(regex *regexp.Regexp) Match {
+ return Match{regex}
+}
+
+func (m Match) IsSatisfied(obj interface{}) bool {
+ str := obj.(string)
+ return m.Regexp.MatchString(str)
+}
+
+func (m Match) DefaultMessage() string {
+ return fmt.Sprintln("Must match", m.Regexp)
+}
+
+var emailPattern = regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$")
+
+type Email struct {
+ Match
+}
+
+func ValidEmail() Email {
+ return Email{Match{emailPattern}}
+}
+
+func (e Email) DefaultMessage() string {
+ return fmt.Sprintln("Must be a valid email address")
+}
+
+const (
+ None = 0
+ IPAny = 1
+ IPv4 = 32 // IPv4 (32 chars)
+ IPv6 = 39 // IPv6(39 chars)
+ IPv4MappedIPv6 = 45 // IP4-mapped IPv6 (45 chars) , Ex) ::FFFF:129.144.52.38
+ IPv4CIDR = IPv4 + 3
+ IPv6CIDR = IPv6 + 3
+ IPv4MappedIPv6CIDR = IPv4MappedIPv6 + 3
+)
+
+// Requires a string(IP Address) to be within IP Pattern type inclusive.
+type IPAddr struct {
+ Vaildtypes []int
+}
+
+// Requires an IP Address string to be exactly a given validation type (IPv4, IPv6, IPv4MappedIPv6, IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR OR IPAny)
+func ValidIPAddr(cktypes ...int) IPAddr {
+
+ for _, cktype := range cktypes {
+
+ if cktype != IPAny && cktype != IPv4 && cktype != IPv6 && cktype != IPv4MappedIPv6 && cktype != IPv4CIDR && cktype != IPv6CIDR && cktype != IPv4MappedIPv6CIDR {
+ return IPAddr{Vaildtypes: []int{None}}
+ }
+ }
+
+ return IPAddr{Vaildtypes: cktypes}
+}
+
+func isWithCIDR(str string, l int) bool {
+
+ if str[l-3] == '/' || str[l-2] == '/' {
+
+ cidr_bit := strings.Split(str, "/")
+ if 2 == len(cidr_bit) {
+ bit, err := strconv.Atoi(cidr_bit[1])
+ //IPv4 : 0~32, IPv6 : 0 ~ 128
+ if err == nil && bit >= 0 && bit <= 128 {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func getIPType(str string, l int) int {
+
+ if l < 3 { //least 3 chars (::F)
+ return None
+ }
+
+ has_dot := strings.Index(str[2:], ".")
+ has_colon := strings.Index(str[2:], ":")
+
+ switch {
+ case has_dot > -1 && has_colon == -1 && l >= 7 && l <= IPv4CIDR:
+ if isWithCIDR(str, l) == true {
+ return IPv4CIDR
+ } else {
+ return IPv4
+ }
+ case has_dot == -1 && has_colon > -1 && l >= 6 && l <= IPv6CIDR:
+ if isWithCIDR(str, l) == true {
+ return IPv6CIDR
+ } else {
+ return IPv6
+ }
+
+ case has_dot > -1 && has_colon > -1 && l >= 14 && l <= IPv4MappedIPv6:
+ if isWithCIDR(str, l) == true {
+ return IPv4MappedIPv6CIDR
+ } else {
+ return IPv4MappedIPv6
+ }
+ }
+
+ return None
+}
+
+func (i IPAddr) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+
+ l := len(str)
+ ret := getIPType(str, l)
+
+ for _, ck := range i.Vaildtypes {
+
+ if ret != None && (ck == ret || ck == IPAny) {
+
+ switch ret {
+ case IPv4, IPv6, IPv4MappedIPv6:
+ ip := net.ParseIP(str)
+
+ if ip != nil {
+ return true
+ }
+
+ case IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR:
+ _, _, err := net.ParseCIDR(str)
+ if err == nil {
+ return true
+ }
+ }
+ }
+ }
+ }
+
+ return false
+}
+
+func (i IPAddr) DefaultMessage() string {
+ return fmt.Sprintln("Must be a vaild IP address")
+}
+
+// Requires a MAC Address string to be exactly
+type MacAddr struct{}
+
+func ValidMacAddr() MacAddr {
+
+ return MacAddr{}
+}
+
+func (m MacAddr) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+ if _, err := net.ParseMAC(str); err == nil {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (m MacAddr) DefaultMessage() string {
+ return fmt.Sprintln("Must be a vaild MAC address")
+}
+
+var domainPattern = regexp.MustCompile(`^(([a-zA-Z0-9-\p{L}]{1,63}\.)?(xn--)?[a-zA-Z0-9\p{L}]+(-[a-zA-Z0-9\p{L}]+)*\.)+[a-zA-Z\p{L}]{2,63}$`)
+
+// Requires a Domain string to be exactly
+type Domain struct {
+ Regexp *regexp.Regexp
+}
+
+func ValidDomain() Domain {
+ return Domain{domainPattern}
+}
+
+func (d Domain) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+
+ l := len(str)
+ //can't exceed 253 chars.
+ if l > 253 {
+ return false
+ }
+
+ //first and last char must be alphanumeric
+ if str[l-1] == 46 || str[0] == 46 {
+ return false
+ }
+
+ return domainPattern.MatchString(str)
+ }
+
+ return false
+}
+
+func (d Domain) DefaultMessage() string {
+ return fmt.Sprintln("Must be a vaild domain address")
+}
+
+var urlPattern = regexp.MustCompile(`^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@#&=+$,A-Za-z0-9\p{L}])+)([).!';/?:,][[:blank:]])?$`)
+
+type URL struct {
+ Domain
+}
+
+func ValidURL() URL {
+ return URL{Domain: ValidDomain()}
+}
+
+func (u URL) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+
+ // TODO : Required lot of testing
+ return urlPattern.MatchString(str)
+ }
+
+ return false
+}
+
+func (u URL) DefaultMessage() string {
+ return fmt.Sprintln("Must be a vaild URL address")
+}
+
+/*
+NORMAL BenchmarkRegex-8 2000000000 0.24 ns/op
+STRICT BenchmarkLoop-8 2000000000 0.01 ns/op
+*/
+const (
+ NORMAL = 0
+ STRICT = 4
+)
+
+// Requires a string to be without invisible characters
+type PureText struct {
+ Mode int
+}
+
+func ValidPureText(m int) PureText {
+ if m != NORMAL && m != STRICT { // Q:required fatal error
+ m = STRICT
+ }
+ return PureText{m}
+}
+
+func isPureTextStrict(str string) (bool, error) {
+
+ l := len(str)
+
+ for i := 0; i < l; i++ {
+
+ c := str[i]
+
+ // deny : control char (00-31 without 9(TAB) and Single 10(LF),13(CR)
+ if c >= 0 && c <= 31 && c != 9 && c != 10 && c != 13 {
+ return false, errors.New("detect control character")
+ }
+
+ // deny : control char (DEL)
+ if c == 127 {
+ return false, errors.New("detect control character (DEL)")
+ }
+
+ //deny : html tag (< ~ >)
+ if c == 60 {
+
+ ds := 0
+ for n := i; n < l; n++ {
+
+ // 60 (<) , 47(/) | 33(!) | 63(?)
+ if str[n] == 60 && n+1 <= l && (str[n+1] == 47 || str[n+1] == 33 || str[n+1] == 63) {
+ ds = 1
+ n += 3 //jump to next char
+ }
+
+ // 62 (>)
+ if ds == 1 && str[n] == 62 {
+ return false, errors.New("detect tag (<[!|?]~>)")
+ }
+ }
+ }
+
+ //deby : html encoded tag (&xxx;)
+ if c == 38 && i+1 <= l && str[i+1] != 35 {
+
+ max := i + 64
+ if max > l {
+ max = l
+ }
+ for n := i; n < max; n++ {
+ if str[n] == 59 {
+ return false, errors.New("detect html encoded ta (&XXX;)")
+ }
+ }
+ }
+ }
+
+ return true, nil
+}
+
+// Requires a string to match a given html tag elements regex pattern
+// referrer : http://www.w3schools.com/Tags/
+var elementPattern = regexp.MustCompile(`(?im)<(?P<tag>(/*\s*|\?*|\!*)(figcaption|expression|blockquote|plaintext|textarea|progress|optgroup|noscript|noframes|menuitem|frameset|fieldset|!DOCTYPE|datalist|colgroup|behavior|basefont|summary|section|isindex|details|caption|bgsound|article|address|acronym|strong|strike|source|select|script|output|option|object|legend|keygen|ilayer|iframe|header|footer|figure|dialog|center|canvas|button|applet|video|track|title|thead|tfoot|tbody|table|style|small|param|meter|layer|label|input|frame|embed|blink|audio|aside|alert|time|span|samp|ruby|meta|menu|mark|main|link|html|head|form|font|code|cite|body|base|area|abbr|xss|xml|wbr|var|svg|sup|sub|pre|nav|map|kbd|ins|img|div|dir|dfn|del|col|big|bdo|bdi|!--|ul|tt|tr|th|td|rt|rp|ol|li|hr|em|dt|dl|dd|br|u|s|q|p|i|b|a|(h[0-9]+)))([^><]*)([><]*)`)
+
+// Requires a string to match a given urlencoded regex pattern
+var urlencodedPattern = regexp.MustCompile(`(?im)(\%[0-9a-fA-F]{1,})`)
+
+// Requires a string to match a given control characters regex pattern (ASCII : 00-08, 11, 12, 14, 15-31)
+var controlcharPattern = regexp.MustCompile(`(?im)([\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+)`)
+
+func isPureTextNormal(str string) (bool, error) {
+
+ decoded_str := html.UnescapeString(str)
+
+ matched_urlencoded := urlencodedPattern.MatchString(decoded_str)
+ if matched_urlencoded == true {
+ temp_buf, err := url.QueryUnescape(decoded_str)
+ if err == nil {
+ decoded_str = temp_buf
+ }
+ }
+
+ matched_element := elementPattern.MatchString(decoded_str)
+ if matched_element == true {
+ return false, errors.New("detect html element")
+ }
+
+ matched_cc := controlcharPattern.MatchString(decoded_str)
+ if matched_cc == true {
+ return false, errors.New("detect control character")
+ }
+
+ return true, nil
+}
+
+func (p PureText) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+
+ var ret bool
+ switch p.Mode {
+ case STRICT:
+ ret, _ = isPureTextStrict(str)
+ case NORMAL:
+ ret, _ = isPureTextStrict(str)
+ }
+ return ret
+ }
+
+ return false
+}
+
+func (p PureText) DefaultMessage() string {
+ return fmt.Sprintln("Must be a vaild Text")
+}
+
+const (
+ ONLY_FILENAME = 0
+ ALLOW_RELATIVE_PATH = 1
+)
+
+const regexDenyFileNameCharList = `[\x00-\x1f|\x21-\x2c|\x3b-\x40|\x5b-\x5e|\x60|\x7b-\x7f]+`
+const regexDenyFileName = `|\x2e\x2e\x2f+`
+
+var checkAllowRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + `)`)
+var checkDenyRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + regexDenyFileName + `)`)
+
+// Requires an string to be sanitary file path
+type FilePath struct {
+ Mode int
+}
+
+func ValidFilePath(m int) FilePath {
+
+ if m != ONLY_FILENAME && m != ALLOW_RELATIVE_PATH {
+ m = ONLY_FILENAME
+ }
+ return FilePath{m}
+}
+
+func (f FilePath) IsSatisfied(obj interface{}) bool {
+
+ if str, ok := obj.(string); ok {
+
+ var ret bool
+ switch f.Mode {
+
+ case ALLOW_RELATIVE_PATH:
+ ret = checkAllowRelativePath.MatchString(str)
+ if ret == false {
+ return true
+ }
+ default: //ONLY_FILENAME
+ ret = checkDenyRelativePath.MatchString(str)
+ if ret == false {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (f FilePath) DefaultMessage() string {
+ return fmt.Sprintln("Must be a unsanitary string")
+}
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel_test
+
+import (
+ "fmt"
+ "github.com/revel/revel"
+ "net"
+ "reflect"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+)
+
+const (
+ errorsMessage = "validation for %s should not be satisfied with %s\n"
+ noErrorsMessage = "validation for %s should be satisfied with %s\n"
+)
+
+type Expect struct {
+ input interface{}
+ expectedResult bool
+ errorMessage string
+}
+
+func performTests(validator revel.Validator, tests []Expect, t *testing.T) {
+ for _, test := range tests {
+ if validator.IsSatisfied(test.input) != test.expectedResult {
+ if test.expectedResult {
+ t.Errorf(noErrorsMessage, reflect.TypeOf(validator), test.errorMessage)
+ } else {
+ t.Errorf(errorsMessage, reflect.TypeOf(validator), test.errorMessage)
+ }
+ }
+ }
+}
+
+func TestRequired(t *testing.T) {
+ tests := []Expect{
+ {nil, false, "nil data"},
+ {"Testing", true, "non-empty string"},
+ {"", false, "empty string"},
+ {true, true, "true boolean"},
+ {false, false, "false boolean"},
+ {1, true, "positive integer"},
+ {-1, true, "negative integer"},
+ {0, false, "0 integer"},
+ {time.Now(), true, "current time"},
+ {time.Time{}, false, "a zero time"},
+ {func() {}, true, "other non-nil data types"},
+ {net.IP(""), false, "empty IP address"},
+ }
+
+ // testing both the struct and the helper method
+ for _, required := range []revel.Required{{}, revel.ValidRequired()} {
+ performTests(required, tests, t)
+ }
+}
+
+func TestMin(t *testing.T) {
+ tests := []Expect{
+ {11, true, "val > min"},
+ {10, true, "val == min"},
+ {9, false, "val < min"},
+ {true, false, "TypeOf(val) != int"},
+ }
+ for _, min := range []revel.Min{{10}, revel.ValidMin(10)} {
+ performTests(min, tests, t)
+ }
+}
+
+func TestMax(t *testing.T) {
+ tests := []Expect{
+ {9, true, "val < max"},
+ {10, true, "val == max"},
+ {11, false, "val > max"},
+ {true, false, "TypeOf(val) != int"},
+ }
+ for _, max := range []revel.Max{{10}, revel.ValidMax(10)} {
+ performTests(max, tests, t)
+ }
+}
+
+func TestRange(t *testing.T) {
+ tests := []Expect{
+ {50, true, "min <= val <= max"},
+ {10, true, "val == min"},
+ {100, true, "val == max"},
+ {9, false, "val < min"},
+ {101, false, "val > max"},
+ }
+
+ goodValidators := []revel.Range{
+ {revel.Min{10}, revel.Max{100}},
+ revel.ValidRange(10, 100),
+ }
+ for _, rangeValidator := range goodValidators {
+ performTests(rangeValidator, tests, t)
+ }
+
+ testsFloat := []Expect{
+ {50, true, "min <= val <= max"},
+ {10.25, true, "val == min"},
+ {100, true, "val == max"},
+ {9, false, "val < min"},
+ {101, false, "val > max"},
+ }
+ goodValidatorsFloat := []revel.Range{
+ {revel.Min{10.25}, revel.Max{100.5}},
+ revel.ValidRangeFloat(10.25, 100.5),
+ }
+ for _, rangeValidator := range goodValidatorsFloat {
+ performTests(rangeValidator, testsFloat, t)
+ }
+
+ tests = []Expect{
+ {10, true, "min == val == max"},
+ {9, false, "val < min && val < max && min == max"},
+ {11, false, "val > min && val > max && min == max"},
+ }
+
+ goodValidators = []revel.Range{
+ {revel.Min{10}, revel.Max{10}},
+ revel.ValidRange(10, 10),
+ }
+ for _, rangeValidator := range goodValidators {
+ performTests(rangeValidator, tests, t)
+ }
+
+ tests = make([]Expect, 7)
+ for i, num := range []int{50, 100, 10, 9, 101, 0, -1} {
+ tests[i] = Expect{
+ num,
+ false,
+ "min > val < max",
+ }
+ }
+ // these are min/max with values swapped, so the min is the high
+ // and max is the low. rangeValidator.IsSatisfied() should ALWAYS
+ // result in false since val can never be greater than min and less
+ // than max when min > max
+ badValidators := []revel.Range{
+ {revel.Min{100}, revel.Max{10}},
+ revel.ValidRange(100, 10),
+ }
+ for _, rangeValidator := range badValidators {
+ performTests(rangeValidator, tests, t)
+ }
+
+ badValidatorsFloat := []revel.Range{
+ {revel.Min{100}, revel.Max{10}},
+ revel.ValidRangeFloat(100, 10),
+ }
+ for _, rangeValidator := range badValidatorsFloat {
+ performTests(rangeValidator, tests, t)
+ }
+}
+
+func TestMinSize(t *testing.T) {
+ greaterThanMessage := "len(val) >= min"
+ tests := []Expect{
+ {"12", true, greaterThanMessage},
+ {"123", true, greaterThanMessage},
+ {[]int{1, 2}, true, greaterThanMessage},
+ {[]int{1, 2, 3}, true, greaterThanMessage},
+ {"", false, "len(val) <= min"},
+ {"手", false, "len(val) <= min"},
+ {[]int{}, false, "len(val) <= min"},
+ {nil, false, "TypeOf(val) != string && TypeOf(val) != slice"},
+ }
+
+ for _, minSize := range []revel.MinSize{{2}, revel.ValidMinSize(2)} {
+ performTests(minSize, tests, t)
+ }
+}
+
+func TestMaxSize(t *testing.T) {
+ lessThanMessage := "len(val) <= max"
+ tests := []Expect{
+ {"", true, lessThanMessage},
+ {"12", true, lessThanMessage},
+ {"ルビー", true, lessThanMessage},
+ {[]int{}, true, lessThanMessage},
+ {[]int{1, 2}, true, lessThanMessage},
+ {[]int{1, 2, 3}, true, lessThanMessage},
+ {"1234", false, "len(val) >= max"},
+ {[]int{1, 2, 3, 4}, false, "len(val) >= max"},
+ }
+ for _, maxSize := range []revel.MaxSize{{3}, revel.ValidMaxSize(3)} {
+ performTests(maxSize, tests, t)
+ }
+}
+
+func TestLength(t *testing.T) {
+ tests := []Expect{
+ {"12", true, "len(val) == length"},
+ {"火箭", true, "len(val) == length"},
+ {[]int{1, 2}, true, "len(val) == length"},
+ {"123", false, "len(val) > length"},
+ {[]int{1, 2, 3}, false, "len(val) > length"},
+ {"1", false, "len(val) < length"},
+ {[]int{1}, false, "len(val) < length"},
+ {nil, false, "TypeOf(val) != string && TypeOf(val) != slice"},
+ }
+ for _, length := range []revel.Length{{2}, revel.ValidLength(2)} {
+ performTests(length, tests, t)
+ }
+}
+
+func TestMatch(t *testing.T) {
+ tests := []Expect{
+ {"bca123", true, `"[abc]{3}\d*" matches "bca123"`},
+ {"bc123", false, `"[abc]{3}\d*" does not match "bc123"`},
+ {"", false, `"[abc]{3}\d*" does not match ""`},
+ }
+ regex := regexp.MustCompile(`[abc]{3}\d*`)
+ for _, match := range []revel.Match{{regex}, revel.ValidMatch(regex)} {
+ performTests(match, tests, t)
+ }
+}
+
+func TestEmail(t *testing.T) {
+ // unicode char included
+ validStartingCharacters := strings.Split("!#$%^&*_+1234567890abcdefghijklmnopqrstuvwxyzñ", "")
+ invalidCharacters := strings.Split(" ()", "")
+
+ definiteInvalidDomains := []string{
+ "", // any empty string (x@)
+ ".com", // only the TLD (x@.com)
+ ".", // only the . (x@.)
+ ".*", // TLD containing symbol (x@.*)
+ "asdf", // no TLD
+ "a!@#$%^&*()+_.com", // characters which are not ASCII/0-9/dash(-) in a domain
+ "-a.com", // host starting with any symbol
+ "a-.com", // host ending with any symbol
+ "aå.com", // domain containing unicode (however, unicode domains do exist in the state of xn--<POINT>.com e.g. å.com = xn--5ca.com)
+ }
+
+ // Email pattern is not exposed
+ emailPattern := regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$")
+ for _, email := range []revel.Email{{revel.Match{emailPattern}}, revel.ValidEmail()} {
+ var currentEmail string
+
+ // test invalid starting chars
+ for _, startingChar := range validStartingCharacters {
+ currentEmail = fmt.Sprintf("%sñbc+123@do-main.com", startingChar)
+ if email.IsSatisfied(currentEmail) {
+ t.Errorf(noErrorsMessage, "starting characters", fmt.Sprintf("email = %s", currentEmail))
+ }
+
+ // validation should fail because of multiple @ symbols
+ currentEmail = fmt.Sprintf("%s@ñbc+123@do-main.com", startingChar)
+ if email.IsSatisfied(currentEmail) {
+ t.Errorf(errorsMessage, "starting characters with multiple @ symbols", fmt.Sprintf("email = %s", currentEmail))
+ }
+
+ // should fail simply because of the invalid char
+ for _, invalidChar := range invalidCharacters {
+ currentEmail = fmt.Sprintf("%sñbc%s+123@do-main.com", startingChar, invalidChar)
+ if email.IsSatisfied(currentEmail) {
+ t.Errorf(errorsMessage, "invalid starting characters", fmt.Sprintf("email = %s", currentEmail))
+ }
+ }
+ }
+
+ // test invalid domains
+ for _, invalidDomain := range definiteInvalidDomains {
+ currentEmail = fmt.Sprintf("a@%s", invalidDomain)
+ if email.IsSatisfied(currentEmail) {
+ t.Errorf(errorsMessage, "invalid domain", fmt.Sprintf("email = %s", currentEmail))
+ }
+ }
+
+ // should always be satisfied
+ if !email.IsSatisfied("t0.est+email123@1abc0-def.com") {
+ t.Errorf(noErrorsMessage, "guaranteed valid email", fmt.Sprintf("email = %s", "t0.est+email123@1abc0-def.com"))
+ }
+
+ // should never be satisfied (this is redundant given the loops above)
+ if email.IsSatisfied("a@xcom") {
+ t.Errorf(noErrorsMessage, "guaranteed invalid email", fmt.Sprintf("email = %s", "a@xcom"))
+ }
+ if email.IsSatisfied("a@@x.com") {
+ t.Errorf(noErrorsMessage, "guaranteed invalid email", fmt.Sprintf("email = %s", "a@@x.com"))
+ }
+ }
+}
+
+func runIPAddrTestfunc(t *testing.T, test_type int, ipaddr_list map[string]bool, msg_fmt string) {
+
+ // generate dataset for test
+ test_ipaddr_list := []Expect{}
+ for ipaddr, expected := range ipaddr_list {
+ test_ipaddr_list = append(test_ipaddr_list, Expect{input: ipaddr, expectedResult: expected, errorMessage: fmt.Sprintf(msg_fmt, ipaddr)})
+ }
+
+ for _, ip_test_list := range []revel.IPAddr{{[]int{test_type}}, revel.ValidIPAddr(test_type)} {
+ performTests(ip_test_list, test_ipaddr_list, t)
+ }
+}
+
+func TestIPAddr(t *testing.T) {
+
+ //IPv4
+ test_ipv4_ipaddrs := map[string]bool{
+ "192.168.1.1": true,
+ "127.0.0.1": true,
+ "10.10.90.12": true,
+ "8.8.8.8": true,
+ "4.4.4.4": true,
+ "912.456.123.123": false,
+ "999.999.999.999": false,
+ "192.192.19.999": false,
+ }
+
+ //IPv4 with CIDR
+ test_ipv4_with_cidr_ipaddrs := map[string]bool{
+ "192.168.1.1/24": true,
+ "127.0.0.1/32": true,
+ "10.10.90.12/8": true,
+ "8.8.8.8/1": true,
+ "4.4.4.4/7": true,
+ "192.168.1.1/99": false,
+ "127.0.0.1/9999": false,
+ "10.10.90.12/33": false,
+ "8.8.8.8/128": false,
+ "4.4.4.4/256": false,
+ }
+
+ //IPv6
+ test_ipv6_ipaddrs := map[string]bool{
+ "2607:f0d0:1002:51::4": true,
+ "2607:f0d0:1002:0051:0000:0000:0000:0004": true,
+ "ff05::1:3": true,
+ "FE80:0000:0000:0000:0202:B3FF:FE1E:8329": true,
+ "FE80::0202:B3FF:FE1E:8329": true,
+ "fe80::202:b3ff:fe1e:8329": true,
+ "fe80:0000:0000:0000:0202:b3ff:fe1e:8329": true,
+ "2001:470:1f09:495::3": true,
+ "2001:470:1f1d:275::1": true,
+ "2600:9000:5304:200::1": true,
+ "2600:9000:5306:d500::1": true,
+ "2600:9000:5301:b600::1": true,
+ "2600:9000:5303:900::1": true,
+ "127:12:12:12:12:12:!2:!2": false,
+ "127.0.0.1": false,
+ "234:23:23:23:23:23:23": false,
+ }
+
+ //IPv6 with CIDR
+ test_ipv6_with_cidr_ipaddrs := map[string]bool{
+ "2000::/5": true,
+ "2000::/15": true,
+ "2001:db8::/33": true,
+ "2001:db8::/48": true,
+ "fc00::/7": true,
+ }
+
+ //IPv4-Mapped Embedded IPv6 Address
+ test_ipv4_mapped_ipv6_ipaddrs := map[string]bool{
+ "2001:470:1f09:495::3:217.126.185.215": true,
+ "2001:470:1f1d:275::1:213.0.69.132": true,
+ "2600:9000:5304:200::1:205.251.196.2": true,
+ "2600:9000:5306:d500::1:205.251.198.213": true,
+ "2600:9000:5301:b600::1:205.251.193.182": true,
+ "2600:9000:5303:900::1:205.251.195.9": true,
+ "0:0:0:0:0:FFFF:222.1.41.90": true,
+ "::FFFF:222.1.41.90": true,
+ "0000:0000:0000:0000:0000:FFFF:12.155.166.101": true,
+ "12.155.166.101": false,
+ "12.12/12": false,
+ }
+
+ runIPAddrTestfunc(t, revel.IPv4, test_ipv4_ipaddrs, "invalid (%s) IPv4 address")
+ runIPAddrTestfunc(t, revel.IPv4CIDR, test_ipv4_with_cidr_ipaddrs, "invalid (%s) IPv4 with CIDR address")
+
+ runIPAddrTestfunc(t, revel.IPv6, test_ipv6_ipaddrs, "invalid (%s) IPv6 address")
+ runIPAddrTestfunc(t, revel.IPv6CIDR, test_ipv6_with_cidr_ipaddrs, "invalid (%s) IPv6 with CIDR address")
+ runIPAddrTestfunc(t, revel.IPv4MappedIPv6, test_ipv4_mapped_ipv6_ipaddrs, "invalid (%s) IPv4-Mapped Embedded IPv6 address")
+}
+
+func TestMacAddr(t *testing.T) {
+
+ macaddr_list := map[string]bool{
+ "02:f3:71:eb:9e:4b": true,
+ "02-f3-71-eb-9e-4b": true,
+ "02f3.71eb.9e4b": true,
+ "87:78:6e:3e:90:40": true,
+ "87-78-6e-3e-90-40": true,
+ "8778.6e3e.9040": true,
+ "e7:28:b9:57:ab:36": true,
+ "e7-28-b9-57-ab-36": true,
+ "e728.b957.ab36": true,
+ "eb:f8:2b:d7:e9:62": true,
+ "eb-f8-2b-d7-e9-62": true,
+ "ebf8.2bd7.e962": true,
+ }
+
+ test_macaddr_list := []Expect{}
+ for macaddr, expected := range macaddr_list {
+ test_macaddr_list = append(test_macaddr_list, Expect{input: macaddr, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) MAC address", macaddr)})
+ }
+
+ for _, mac_test_list := range []revel.MacAddr{{}, revel.ValidMacAddr()} {
+ performTests(mac_test_list, test_macaddr_list, t)
+ }
+}
+
+func TestDomain(t *testing.T) {
+
+ test_domains := map[string]bool{
+ "대한민국.xn-korea.co.kr": true,
+ "google.com": true,
+ "masełkowski.pl": true,
+ "maselkowski.pl": true,
+ "m.maselkowski.pl": true,
+ "www.masełkowski.pl.com": true,
+ "xn--masekowski-d0b.pl": true,
+ "中国互联网络信息中心.中国": true,
+ "masełkowski.pl.": false,
+ "中国互联网络信息中心.xn--masekowski-d0b": false,
+ "a.jp": true,
+ "a.co": true,
+ "a.co.jp": true,
+ "a.co.or": true,
+ "a.or.kr": true,
+ "qwd-qwdqwd.com": true,
+ "qwd-qwdqwd.co_m": false,
+ "qwd-qwdqwd.c": false,
+ "qwd-qwdqwd.-12": false,
+ "qwd-qwdqwd.1212": false,
+ "qwd-qwdqwd.org": true,
+ "qwd-qwdqwd.ac.kr": true,
+ "qwd-qwdqwd.gov": true,
+ "chicken.beer": true,
+ "aa.xyz": true,
+ "google.asn.au": true,
+ "google.com.au": true,
+ "google.net.au": true,
+ "google.priv.at": true,
+ "google.ac.at": true,
+ "google.gv.at": true,
+ "google.avocat.fr": true,
+ "google.geek.nz": true,
+ "google.gen.nz": true,
+ "google.kiwi.nz": true,
+ "google.org.il": true,
+ "google.net.il": true,
+ "www.google.edu.au": true,
+ "www.google.gov.au": true,
+ "www.google.csiro.au": true,
+ "www.google.act.au": true,
+ "www.google.avocat.fr": true,
+ "www.google.aeroport.fr": true,
+ "www.google.co.nz": true,
+ "www.google.geek.nz": true,
+ "www.google.gen.nz": true,
+ "www.google.kiwi.nz": true,
+ "www.google.parliament.nz": true,
+ "www.google.muni.il": true,
+ "www.google.idf.il": true,
+ }
+
+ tests := []Expect{}
+
+ for domain, expected := range test_domains {
+ tests = append(tests, Expect{input: domain, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) domain", domain)})
+ }
+
+ for _, domain := range []revel.Domain{{}, revel.ValidDomain()} {
+ performTests(domain, tests, t)
+ }
+}
+
+func TestURL(t *testing.T) {
+
+ test_urls := map[string]bool{
+ "https://www.google.co.kr/url?sa=t&rct=j&q=&esrc=s&source=web": true,
+ "http://stackoverflow.com/questions/27812164/can-i-import-3rd-party-package-into-golang-playground": true,
+ "https://tour.golang.org/welcome/4": true,
+ "https://revel.github.io/": true,
+ "https://github.com/revel/revel/commit/bd1d083ee4345e919b3bca1e4c42ca682525e395#diff-972a2b2141d27e9d7a8a4149a7e28eef": true,
+ "https://github.com/ndevilla/iniparser/pull/82#issuecomment-261817064": true,
+ "http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=golang": true,
+ "http://www.baidu.com/link?url=DrWkM_beo2M5kB5sLYnItKSQ0Ib3oDhKcPprdtLzAWNfFt_VN5oyD3KwnAKT6Xsk": true,
+ }
+
+ tests := []Expect{}
+
+ for url, expected := range test_urls {
+ tests = append(tests, Expect{input: url, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) url", url)})
+ }
+
+ for _, url := range []revel.URL{{}, revel.ValidURL()} {
+ performTests(url, tests, t)
+ }
+
+}
+
+func TestPureTextNormal(t *testing.T) {
+
+ test_txts := map[string]bool{
+ `<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+ `a\r\nb<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+ `Foo<script type="text/javascript">alert(1337)</script>Bar`: false,
+ `Foo<12>Bar`: true,
+ `Foo<>Bar`: true,
+ `Foo</br>Bar`: false,
+ `Foo <!-- Bar --> Baz`: false,
+ `I <3 Ponies!`: true,
+ `I   like Golang\t\n`: true,
+ `I & like Golang\t\n`: false,
+ `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="console" /> </root> </log4j:configuration>`: false,
+ `I like Golang\r\n`: true,
+ `I like Golang\r\na`: true,
+ "I   like Golang\t\n": true,
+ "I & like Golang\t\n": false,
+ `ハイレゾ対応ウォークマン®、ヘッドホン、スピーカー「Winter Gift Collection ~Presented by JUJU~」をソニーストアにて販売開始`: true,
+ `VAIOパーソナルコンピューター type T TZシリーズ 無償点検・修理のお知らせとお詫び(2009年10月15日更新)`: true,
+ `把百度设为主页关于百度About Baidu百度推广`: true,
+ `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About++Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+ `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About%20%20Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+ `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</>qwdoijqwdoiqjd`: true,
+ `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</a>qwdoijqwdoiqjd`: false,
+ }
+
+ tests := []Expect{}
+
+ for txt, expected := range test_txts {
+ tests = append(tests, Expect{input: txt, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%#v) text", txt)})
+ }
+
+ // normal
+ for _, txt := range []revel.PureText{{revel.NORMAL}, revel.ValidPureText(revel.NORMAL)} {
+ performTests(txt, tests, t)
+ }
+}
+
+func TestPureTextStrict(t *testing.T) {
+
+ test_txts := map[string]bool{
+ `<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+ `a\r\nb<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+ `Foo<script type="text/javascript">alert(1337)</script>Bar`: false,
+ `Foo<12>Bar`: true,
+ `Foo<>Bar`: true,
+ `Foo</br>Bar`: false,
+ `Foo <!-- Bar --> Baz`: false,
+ `I <3 Ponies!`: true,
+ `I   like Golang\t\n`: true,
+ `I & like Golang\t\n`: false,
+ `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'> <ender name="console" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p 1}:%L - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="console" /> </root> </log4j:configuration>`: false,
+ `I like Golang\r\n`: true,
+ `I like Golang\r\na`: true,
+ "I   like Golang\t\n": true,
+ "I & like Golang\t\n": false,
+ `ハイレゾ対応ウォークマン®、ヘッドホン、スピーカー「Winter Gift Collection ~Presented by JUJU~」をソニーストアにて販売開始`: true,
+ `VAIOパーソナルコンピューター type T TZシリーズ 無償点検・修理のお知らせとお詫び(2009年10月15日更新)`: true,
+ `把百度设为主页关于百度About Baidu百度推广`: true,
+ `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About++Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+ `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About%20%20Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+ `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</>qwdoijqwdoiqjd`: true,
+ `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</a>qwdoijqwdoiqjd`: false,
+ }
+
+ tests := []Expect{}
+
+ for txt, expected := range test_txts {
+ tests = append(tests, Expect{input: txt, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%#v) text", txt)})
+ }
+
+ // strict
+ for _, txt := range []revel.PureText{{revel.STRICT}, revel.ValidPureText(revel.STRICT)} {
+ performTests(txt, tests, t)
+ }
+}
+
+func TestFilePathOnlyFilePath(t *testing.T) {
+
+ test_filepaths := map[string]bool{
+ "../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+ `../../qwdqwdqwd/..
+ /qwdqwdqwd.txt`: false,
+ "\t../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+ `\16../../qwdqwdqwd/../qwdqwdqwd.txt`: false,
+ `../../qwdqwdqwd/..\ 2/qwdqwdqwd.txt`: false,
+ "../../etc/passwd": false,
+ "a.txt;rm -rf /": false,
+ "sudo rm -rf ../": false,
+ "a-1-s-d-v-we-wd_+qwd-qwd-qwd.txt": false,
+ "a-qwdqwd_qwdqwdqwd-123.txt": true,
+ "a.txt": true,
+ "a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt": true,
+ }
+
+ tests := []Expect{}
+
+ for filepath, expected := range test_filepaths {
+ tests = append(tests, Expect{input: filepath, expectedResult: expected, errorMessage: fmt.Sprintf("unsanitary (%#v) string", filepath)})
+ }
+
+ // filename without relative path
+ for _, filepath := range []revel.FilePath{{revel.ONLY_FILENAME}, revel.ValidFilePath(revel.ONLY_FILENAME)} {
+ performTests(filepath, tests, t)
+ }
+}
+
+func TestFilePathAllowRelativePath(t *testing.T) {
+
+ test_filepaths := map[string]bool{
+ "../../qwdqwdqwd/../qwdqwdqwd.txt": true,
+ `../../qwdqwdqwd/..
+ /qwdqwdqwd.txt`: false,
+ "\t../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+ `\16../../qwdqwdqwd/../qwdqwdqwd.txt`: false,
+ `../../qwdqwdqwd/..\ 2/qwdqwdqwd.txt`: false,
+ "../../etc/passwd": true,
+ "a.txt;rm -rf /": false,
+ "sudo rm -rf ../": true,
+ "a-1-s-d-v-we-wd_+qwd-qwd-qwd.txt": false,
+ "a-qwdqwd_qwdqwdqwd-123.txt": true,
+ "a.txt": true,
+ "a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt": true,
+ "/asdasd/asdasdasd/qwdqwd_qwdqwd/12-12/a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt": true,
+ }
+
+ tests := []Expect{}
+
+ for filepath, expected := range test_filepaths {
+ tests = append(tests, Expect{input: filepath, expectedResult: expected, errorMessage: fmt.Sprintf("unsanitary (%#v) string", filepath)})
+ }
+
+ // filename with relative path
+ for _, filepath := range []revel.FilePath{{revel.ALLOW_RELATIVE_PATH}, revel.ValidFilePath(revel.ALLOW_RELATIVE_PATH)} {
+ performTests(filepath, tests, t)
+ }
+}
--- /dev/null
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+const (
+ // Version current Revel version
+ Version = "0.21.0"
+
+ // BuildDate latest commit/release date
+ BuildDate = "2018-10-30"
+
+ // MinimumGoVersion minimum required Go version for Revel
+ MinimumGoVersion = ">= go1.8"
+)
--- /dev/null
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "gopkg.in/fsnotify/fsnotify.v1"
+ "time"
+)
+
+// Listener is an interface for receivers of filesystem events.
+type Listener interface {
+ // Refresh is invoked by the watcher on relevant filesystem events.
+ // If the listener returns an error, it is served to the user on the current request.
+ Refresh() *Error
+}
+
+// DiscerningListener allows the receiver to selectively watch files.
+type DiscerningListener interface {
+ Listener
+ WatchDir(info os.FileInfo) bool
+ WatchFile(basename string) bool
+}
+
+// Watcher allows listeners to register to be notified of changes under a given
+// directory.
+type Watcher struct {
+ serial bool // true to process events in serial
+ watchers []*fsnotify.Watcher // Parallel arrays of watcher/listener pairs.
+ listeners []Listener // List of listeners for watcher
+ forceRefresh bool // True to force the refresh
+ lastError int // The last error found
+ notifyMutex sync.Mutex // The mutext to serialize watches
+ refreshTimer *time.Timer // The timer to countdown the next refresh
+ timerMutex *sync.Mutex // A mutex to prevent concurrent updates
+ refreshChannel chan *Error // The error channel to listen to when waiting for a refresh
+ refreshChannelCount int // The number of clients listening on the channel
+ refreshTimerMS time.Duration // The number of milliseconds between refreshing builds
+}
+
+func NewWatcher() *Watcher {
+ return &Watcher{
+ forceRefresh: true,
+ lastError: -1,
+ refreshTimerMS: time.Duration(Config.IntDefault("watch.rebuild.delay", 10)),
+ timerMutex: &sync.Mutex{},
+ refreshChannel: make(chan *Error, 10),
+ refreshChannelCount: 0,
+ }
+}
+
+// Listen registers for events within the given root directories (recursively).
+func (w *Watcher) Listen(listener Listener, roots ...string) {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ utilLog.Fatal("Watcher: Failed to create watcher", "error", err)
+ }
+
+ // Replace the unbuffered Event channel with a buffered one.
+ // Otherwise multiple change events only come out one at a time, across
+ // multiple page views. (There appears no way to "pump" the events out of
+ // the watcher)
+ // This causes a notification when you do a check in go, since you are modifying a buffer in use
+ watcher.Events = make(chan fsnotify.Event, 100)
+ watcher.Errors = make(chan error, 10)
+
+ // Walk through all files / directories under the root, adding each to watcher.
+ for _, p := range roots {
+ // is the directory / file a symlink?
+ f, err := os.Lstat(p)
+ if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
+ var realPath string
+ realPath, err = filepath.EvalSymlinks(p)
+ if err != nil {
+ panic(err)
+ }
+ p = realPath
+ }
+
+ fi, err := os.Stat(p)
+ if err != nil {
+ utilLog.Error("Watcher: Failed to stat watched path, code will continue but auto updates will not work", "path", p, "error", err)
+ continue
+ }
+
+ // If it is a file, watch that specific file.
+ if !fi.IsDir() {
+ err = watcher.Add(p)
+ if err != nil {
+ utilLog.Error("Watcher: Failed to watch, code will continue but auto updates will not work", "path", p, "error", err)
+ }
+ continue
+ }
+
+ var watcherWalker func(path string, info os.FileInfo, err error) error
+
+ watcherWalker = func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ utilLog.Error("Watcher: Error walking path:", "error", err)
+ return nil
+ }
+
+ if info.IsDir() {
+ if dl, ok := listener.(DiscerningListener); ok {
+ if !dl.WatchDir(info) {
+ return filepath.SkipDir
+ }
+ }
+
+ err := watcher.Add(path)
+ if err != nil {
+ utilLog.Error("Watcher: Failed to watch this path, code will continue but auto updates will not work", "path", path, "error", err)
+ }
+ }
+ return nil
+ }
+
+ // Else, walk the directory tree.
+ err = Walk(p, watcherWalker)
+ if err != nil {
+ utilLog.Error("Watcher: Failed to walk directory, code will continue but auto updates will not work", "path", p, "error", err)
+ }
+ }
+
+ if w.eagerRebuildEnabled() {
+ // Create goroutine to notify file changes in real time
+ go w.NotifyWhenUpdated(listener, watcher)
+ }
+
+ w.watchers = append(w.watchers, watcher)
+ w.listeners = append(w.listeners, listener)
+}
+
+// NotifyWhenUpdated notifies the watcher when a file event is received.
+func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) {
+
+ for {
+ select {
+ case ev := <-watcher.Events:
+ if w.rebuildRequired(ev, listener) {
+ // Serialize listener.Refresh() calls.
+ if w.serial {
+ // Serialize listener.Refresh() calls.
+ w.notifyMutex.Lock()
+
+ if err := listener.Refresh(); err != nil {
+ utilLog.Error("Watcher: Listener refresh reported error:", "error", err)
+ }
+ w.notifyMutex.Unlock()
+ } else {
+ // Run refresh in parallel
+ go func() {
+ w.notifyInProcess(listener)
+ }()
+ }
+ }
+ case <-watcher.Errors:
+ continue
+ }
+ }
+}
+
+// Notify causes the watcher to forward any change events to listeners.
+// It returns the first (if any) error returned.
+func (w *Watcher) Notify() *Error {
+ // Serialize Notify() calls.
+ w.notifyMutex.Lock()
+ defer w.notifyMutex.Unlock()
+
+ for i, watcher := range w.watchers {
+ listener := w.listeners[i]
+
+ // Pull all pending events / errors from the watcher.
+ refresh := false
+ for {
+ select {
+ case ev := <-watcher.Events:
+ if w.rebuildRequired(ev, listener) {
+ refresh = true
+ }
+ continue
+ case <-watcher.Errors:
+ continue
+ default:
+ // No events left to pull
+ }
+ break
+ }
+
+ if w.forceRefresh || refresh || w.lastError == i {
+ var err *Error
+ if w.serial {
+ err = listener.Refresh()
+ } else {
+ err = w.notifyInProcess(listener)
+ }
+
+ if err != nil {
+ w.lastError = i
+ return err
+ }
+ }
+ }
+
+ w.forceRefresh = false
+ w.lastError = -1
+ return nil
+}
+
+// Build a queue for refresh notifications
+// this will not return until one of the queue completes
+func (w *Watcher) notifyInProcess(listener Listener) (err *Error) {
+ shouldReturn := false
+ // This code block ensures that either a timer is created
+ // or that a process would be added the the h.refreshChannel
+ func() {
+ w.timerMutex.Lock()
+ defer w.timerMutex.Unlock()
+ // If we are in the process of a rebuild, forceRefresh will always be true
+ w.forceRefresh = true
+ if w.refreshTimer != nil {
+ utilLog.Info("Found existing timer running, resetting")
+ w.refreshTimer.Reset(time.Millisecond * w.refreshTimerMS)
+ shouldReturn = true
+ w.refreshChannelCount++
+ } else {
+ w.refreshTimer = time.NewTimer(time.Millisecond * w.refreshTimerMS)
+ }
+ }()
+
+ // If another process is already waiting for the timer this one
+ // only needs to return the output from the channel
+ if shouldReturn {
+ return <-w.refreshChannel
+ }
+ utilLog.Info("Waiting for refresh timer to expire")
+ <-w.refreshTimer.C
+ w.timerMutex.Lock()
+
+ // Ensure the queue is properly dispatched even if a panic occurs
+ defer func() {
+ for x := 0; x < w.refreshChannelCount; x++ {
+ w.refreshChannel <- err
+ }
+ w.refreshChannelCount = 0
+ w.refreshTimer = nil
+ w.timerMutex.Unlock()
+ }()
+
+ err = listener.Refresh()
+ if err != nil {
+ utilLog.Info("Watcher: Recording error last build, setting rebuild on", "error", err)
+ } else {
+ w.lastError = -1
+ w.forceRefresh = false
+ }
+ utilLog.Info("Rebuilt, result", "error", err)
+ return
+}
+
+// If watch.mode is set to eager, the application is rebuilt immediately
+// when a source file is changed.
+// This feature is available only in dev mode.
+func (w *Watcher) eagerRebuildEnabled() bool {
+ return Config.BoolDefault("mode.dev", true) &&
+ Config.BoolDefault("watch", true) &&
+ Config.StringDefault("watch.mode", "normal") == "eager"
+}
+
+func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool {
+ // Ignore changes to dotfiles.
+ if strings.HasPrefix(filepath.Base(ev.Name), ".") {
+ return false
+ }
+
+ if dl, ok := listener.(DiscerningListener); ok {
+ if !dl.WatchFile(ev.Name) || ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+ return false
+ }
+ }
+ return true
+}
+
+var WatchFilter = func(c *Controller, fc []Filter) {
+ if MainWatcher != nil {
+ err := MainWatcher.Notify()
+ if err != nil {
+ c.Result = c.RenderError(err)
+ return
+ }
+ }
+ fc[0](c, fc[1:])
+}