From 1d1ee6961c93781e1187d8c7faa868da6b2f01f4 Mon Sep 17 00:00:00 2001 From: Trevor Tao Date: Fri, 10 Jan 2020 00:00:15 +0800 Subject: [PATCH] Add API Framework Revel Source Files Add API framework Revel source files, as the basis for api framework. Signed-off-by: Trevor Tao Change-Id: I8c60569238a66538e86a2cc98a0d6473df2f8e72 --- src/foundation/api/revel/.codebeatsettings | 11 + src/foundation/api/revel/.travis.yml | 67 ++ src/foundation/api/revel/AUTHORS | 1 + src/foundation/api/revel/CHANGELOG.md | 623 +++++++++++++++ src/foundation/api/revel/CONTRIBUTING.md | 162 ++++ src/foundation/api/revel/LICENSE | 20 + src/foundation/api/revel/README.md | 56 ++ src/foundation/api/revel/before_after_filter.go | 60 ++ src/foundation/api/revel/binder.go | 543 +++++++++++++ src/foundation/api/revel/binder_test.go | 419 ++++++++++ src/foundation/api/revel/cache/cache.go | 145 ++++ src/foundation/api/revel/cache/cache_test.go | 253 ++++++ src/foundation/api/revel/cache/init.go | 60 ++ src/foundation/api/revel/cache/inmemory.go | 163 ++++ src/foundation/api/revel/cache/inmemory_test.go | 44 ++ src/foundation/api/revel/cache/memcached.go | 118 +++ src/foundation/api/revel/cache/memcached_test.go | 56 ++ src/foundation/api/revel/cache/redis.go | 273 +++++++ src/foundation/api/revel/cache/redis_test.go | 66 ++ src/foundation/api/revel/cache/serialization.go | 78 ++ .../api/revel/cache/serialization_test.go | 87 +++ src/foundation/api/revel/compress.go | 395 ++++++++++ src/foundation/api/revel/compress_test.go | 61 ++ src/foundation/api/revel/conf/mime-types.conf | 545 +++++++++++++ src/foundation/api/revel/controller.go | 546 +++++++++++++ src/foundation/api/revel/controller_type.go | 164 ++++ src/foundation/api/revel/errors.go | 155 ++++ src/foundation/api/revel/event.go | 57 ++ src/foundation/api/revel/event_test.go | 24 + src/foundation/api/revel/fakeapp_test.go | 152 ++++ src/foundation/api/revel/field.go | 108 +++ src/foundation/api/revel/filter.go | 31 + src/foundation/api/revel/filterconfig.go | 223 ++++++ src/foundation/api/revel/filterconfig_test.go | 141 ++++ src/foundation/api/revel/flash.go | 78 ++ src/foundation/api/revel/http.go | 489 ++++++++++++ src/foundation/api/revel/i18n.go | 245 ++++++ src/foundation/api/revel/i18n_test.go | 273 +++++++ src/foundation/api/revel/intercept.go | 249 ++++++ src/foundation/api/revel/intercept_test.go | 85 +++ src/foundation/api/revel/invoker.go | 50 ++ src/foundation/api/revel/invoker_test.go | 134 ++++ src/foundation/api/revel/libs.go | 86 +++ src/foundation/api/revel/libs_test.go | 38 + src/foundation/api/revel/logger.go | 65 ++ .../api/revel/logger/composite_multihandler.go | 174 +++++ src/foundation/api/revel/logger/doc.go | 15 + src/foundation/api/revel/logger/handlers.go | 210 +++++ src/foundation/api/revel/logger/init.go | 189 +++++ src/foundation/api/revel/logger/init_test.go | 273 +++++++ .../api/revel/logger/log_function_map.go | 37 + src/foundation/api/revel/logger/logger.go | 203 +++++ src/foundation/api/revel/logger/revel_logger.go | 142 ++++ src/foundation/api/revel/logger/terminal_format.go | 245 ++++++ src/foundation/api/revel/logger/utils.go | 110 +++ src/foundation/api/revel/logger/wrap_handlers.go | 98 +++ src/foundation/api/revel/model/revel_container.go | 14 + src/foundation/api/revel/module.go | 213 ++++++ src/foundation/api/revel/namespace.go | 37 + src/foundation/api/revel/panic.go | 50 ++ src/foundation/api/revel/params.go | 180 +++++ src/foundation/api/revel/params_test.go | 182 +++++ src/foundation/api/revel/results.go | 513 +++++++++++++ src/foundation/api/revel/results_test.go | 73 ++ src/foundation/api/revel/revel.go | 301 ++++++++ src/foundation/api/revel/revel_hooks.go | 103 +++ src/foundation/api/revel/revel_test.go | 13 + src/foundation/api/revel/router.go | 846 +++++++++++++++++++++ src/foundation/api/revel/router_test.go | 678 +++++++++++++++++ src/foundation/api/revel/server-engine.go | 229 ++++++ src/foundation/api/revel/server.go | 153 ++++ src/foundation/api/revel/server_adapter_go.go | 647 ++++++++++++++++ src/foundation/api/revel/server_test.go | 148 ++++ src/foundation/api/revel/session/init.go | 10 + src/foundation/api/revel/session/session.go | 364 +++++++++ .../api/revel/session/session_cookie_test.go | 76 ++ src/foundation/api/revel/session/session_test.go | 64 ++ src/foundation/api/revel/session_adapter_cookie.go | 144 ++++ src/foundation/api/revel/session_engine.go | 35 + src/foundation/api/revel/session_filter.go | 25 + src/foundation/api/revel/template.go | 477 ++++++++++++ src/foundation/api/revel/template_adapter_go.go | 129 ++++ src/foundation/api/revel/template_engine.go | 118 +++ src/foundation/api/revel/template_functions.go | 341 +++++++++ src/foundation/api/revel/templates/errors/403.html | 16 + src/foundation/api/revel/templates/errors/403.json | 4 + src/foundation/api/revel/templates/errors/403.txt | 3 + src/foundation/api/revel/templates/errors/403.xml | 1 + .../api/revel/templates/errors/404-dev.html | 63 ++ src/foundation/api/revel/templates/errors/404.html | 26 + src/foundation/api/revel/templates/errors/404.json | 4 + src/foundation/api/revel/templates/errors/404.txt | 3 + src/foundation/api/revel/templates/errors/404.xml | 1 + src/foundation/api/revel/templates/errors/405.html | 16 + src/foundation/api/revel/templates/errors/405.json | 4 + src/foundation/api/revel/templates/errors/405.txt | 3 + src/foundation/api/revel/templates/errors/405.xml | 1 + .../api/revel/templates/errors/500-dev.html | 133 ++++ src/foundation/api/revel/templates/errors/500.html | 16 + src/foundation/api/revel/templates/errors/500.json | 4 + src/foundation/api/revel/templates/errors/500.txt | 15 + src/foundation/api/revel/templates/errors/500.xml | 4 + .../api/revel/testdata/app/views/footer.html | 8 + .../api/revel/testdata/app/views/header.html | 45 ++ .../api/revel/testdata/app/views/hotels/show.html | 37 + src/foundation/api/revel/testdata/conf/app.conf | 44 ++ src/foundation/api/revel/testdata/conf/routes | 16 + .../api/revel/testdata/i18n/config/test_app.conf | 33 + .../revel/testdata/i18n/messages/dutch_messages.nl | 9 + .../testdata/i18n/messages/english_messages.en | 21 + .../testdata/i18n/messages/english_messages2.en | 1 + .../i18n/messages/invalid_message_file_name.txt | 0 .../api/revel/testdata/public/js/sessvars.js | 1 + src/foundation/api/revel/testing/testsuite.go | 411 ++++++++++ src/foundation/api/revel/testing/testsuite_test.go | 303 ++++++++ src/foundation/api/revel/util.go | 280 +++++++ src/foundation/api/revel/util_test.go | 95 +++ src/foundation/api/revel/utils/simplestack.go | 103 +++ src/foundation/api/revel/utils/simplestack_test.go | 127 ++++ src/foundation/api/revel/validation.go | 334 ++++++++ src/foundation/api/revel/validation_test.go | 106 +++ src/foundation/api/revel/validators.go | 633 +++++++++++++++ src/foundation/api/revel/validators_test.go | 640 ++++++++++++++++ src/foundation/api/revel/version.go | 16 + src/foundation/api/revel/watcher.go | 299 ++++++++ 125 files changed, 19437 insertions(+) create mode 100644 src/foundation/api/revel/.codebeatsettings create mode 100644 src/foundation/api/revel/.travis.yml create mode 100644 src/foundation/api/revel/AUTHORS create mode 100644 src/foundation/api/revel/CHANGELOG.md create mode 100644 src/foundation/api/revel/CONTRIBUTING.md create mode 100644 src/foundation/api/revel/LICENSE create mode 100644 src/foundation/api/revel/README.md create mode 100644 src/foundation/api/revel/before_after_filter.go create mode 100644 src/foundation/api/revel/binder.go create mode 100644 src/foundation/api/revel/binder_test.go create mode 100644 src/foundation/api/revel/cache/cache.go create mode 100644 src/foundation/api/revel/cache/cache_test.go create mode 100644 src/foundation/api/revel/cache/init.go create mode 100644 src/foundation/api/revel/cache/inmemory.go create mode 100644 src/foundation/api/revel/cache/inmemory_test.go create mode 100644 src/foundation/api/revel/cache/memcached.go create mode 100644 src/foundation/api/revel/cache/memcached_test.go create mode 100644 src/foundation/api/revel/cache/redis.go create mode 100644 src/foundation/api/revel/cache/redis_test.go create mode 100644 src/foundation/api/revel/cache/serialization.go create mode 100644 src/foundation/api/revel/cache/serialization_test.go create mode 100644 src/foundation/api/revel/compress.go create mode 100644 src/foundation/api/revel/compress_test.go create mode 100644 src/foundation/api/revel/conf/mime-types.conf create mode 100644 src/foundation/api/revel/controller.go create mode 100644 src/foundation/api/revel/controller_type.go create mode 100644 src/foundation/api/revel/errors.go create mode 100644 src/foundation/api/revel/event.go create mode 100644 src/foundation/api/revel/event_test.go create mode 100644 src/foundation/api/revel/fakeapp_test.go create mode 100644 src/foundation/api/revel/field.go create mode 100644 src/foundation/api/revel/filter.go create mode 100644 src/foundation/api/revel/filterconfig.go create mode 100644 src/foundation/api/revel/filterconfig_test.go create mode 100644 src/foundation/api/revel/flash.go create mode 100644 src/foundation/api/revel/http.go create mode 100644 src/foundation/api/revel/i18n.go create mode 100644 src/foundation/api/revel/i18n_test.go create mode 100644 src/foundation/api/revel/intercept.go create mode 100644 src/foundation/api/revel/intercept_test.go create mode 100644 src/foundation/api/revel/invoker.go create mode 100644 src/foundation/api/revel/invoker_test.go create mode 100644 src/foundation/api/revel/libs.go create mode 100644 src/foundation/api/revel/libs_test.go create mode 100644 src/foundation/api/revel/logger.go create mode 100644 src/foundation/api/revel/logger/composite_multihandler.go create mode 100644 src/foundation/api/revel/logger/doc.go create mode 100644 src/foundation/api/revel/logger/handlers.go create mode 100644 src/foundation/api/revel/logger/init.go create mode 100644 src/foundation/api/revel/logger/init_test.go create mode 100644 src/foundation/api/revel/logger/log_function_map.go create mode 100644 src/foundation/api/revel/logger/logger.go create mode 100644 src/foundation/api/revel/logger/revel_logger.go create mode 100644 src/foundation/api/revel/logger/terminal_format.go create mode 100644 src/foundation/api/revel/logger/utils.go create mode 100644 src/foundation/api/revel/logger/wrap_handlers.go create mode 100644 src/foundation/api/revel/model/revel_container.go create mode 100644 src/foundation/api/revel/module.go create mode 100644 src/foundation/api/revel/namespace.go create mode 100644 src/foundation/api/revel/panic.go create mode 100644 src/foundation/api/revel/params.go create mode 100644 src/foundation/api/revel/params_test.go create mode 100644 src/foundation/api/revel/results.go create mode 100644 src/foundation/api/revel/results_test.go create mode 100644 src/foundation/api/revel/revel.go create mode 100644 src/foundation/api/revel/revel_hooks.go create mode 100644 src/foundation/api/revel/revel_test.go create mode 100644 src/foundation/api/revel/router.go create mode 100644 src/foundation/api/revel/router_test.go create mode 100644 src/foundation/api/revel/server-engine.go create mode 100644 src/foundation/api/revel/server.go create mode 100644 src/foundation/api/revel/server_adapter_go.go create mode 100644 src/foundation/api/revel/server_test.go create mode 100644 src/foundation/api/revel/session/init.go create mode 100644 src/foundation/api/revel/session/session.go create mode 100644 src/foundation/api/revel/session/session_cookie_test.go create mode 100644 src/foundation/api/revel/session/session_test.go create mode 100644 src/foundation/api/revel/session_adapter_cookie.go create mode 100644 src/foundation/api/revel/session_engine.go create mode 100644 src/foundation/api/revel/session_filter.go create mode 100644 src/foundation/api/revel/template.go create mode 100644 src/foundation/api/revel/template_adapter_go.go create mode 100644 src/foundation/api/revel/template_engine.go create mode 100644 src/foundation/api/revel/template_functions.go create mode 100644 src/foundation/api/revel/templates/errors/403.html create mode 100644 src/foundation/api/revel/templates/errors/403.json create mode 100644 src/foundation/api/revel/templates/errors/403.txt create mode 100644 src/foundation/api/revel/templates/errors/403.xml create mode 100644 src/foundation/api/revel/templates/errors/404-dev.html create mode 100644 src/foundation/api/revel/templates/errors/404.html create mode 100644 src/foundation/api/revel/templates/errors/404.json create mode 100644 src/foundation/api/revel/templates/errors/404.txt create mode 100644 src/foundation/api/revel/templates/errors/404.xml create mode 100644 src/foundation/api/revel/templates/errors/405.html create mode 100644 src/foundation/api/revel/templates/errors/405.json create mode 100644 src/foundation/api/revel/templates/errors/405.txt create mode 100644 src/foundation/api/revel/templates/errors/405.xml create mode 100644 src/foundation/api/revel/templates/errors/500-dev.html create mode 100644 src/foundation/api/revel/templates/errors/500.html create mode 100644 src/foundation/api/revel/templates/errors/500.json create mode 100644 src/foundation/api/revel/templates/errors/500.txt create mode 100644 src/foundation/api/revel/templates/errors/500.xml create mode 100644 src/foundation/api/revel/testdata/app/views/footer.html create mode 100644 src/foundation/api/revel/testdata/app/views/header.html create mode 100644 src/foundation/api/revel/testdata/app/views/hotels/show.html create mode 100644 src/foundation/api/revel/testdata/conf/app.conf create mode 100644 src/foundation/api/revel/testdata/conf/routes create mode 100644 src/foundation/api/revel/testdata/i18n/config/test_app.conf create mode 100644 src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl create mode 100644 src/foundation/api/revel/testdata/i18n/messages/english_messages.en create mode 100644 src/foundation/api/revel/testdata/i18n/messages/english_messages2.en create mode 100644 src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt create mode 100644 src/foundation/api/revel/testdata/public/js/sessvars.js create mode 100644 src/foundation/api/revel/testing/testsuite.go create mode 100644 src/foundation/api/revel/testing/testsuite_test.go create mode 100644 src/foundation/api/revel/util.go create mode 100644 src/foundation/api/revel/util_test.go create mode 100644 src/foundation/api/revel/utils/simplestack.go create mode 100644 src/foundation/api/revel/utils/simplestack_test.go create mode 100644 src/foundation/api/revel/validation.go create mode 100644 src/foundation/api/revel/validation_test.go create mode 100644 src/foundation/api/revel/validators.go create mode 100644 src/foundation/api/revel/validators_test.go create mode 100644 src/foundation/api/revel/version.go create mode 100644 src/foundation/api/revel/watcher.go diff --git a/src/foundation/api/revel/.codebeatsettings b/src/foundation/api/revel/.codebeatsettings new file mode 100644 index 0000000..f921ed8 --- /dev/null +++ b/src/foundation/api/revel/.codebeatsettings @@ -0,0 +1,11 @@ +{ + "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 diff --git a/src/foundation/api/revel/.travis.yml b/src/foundation/api/revel/.travis.yml new file mode 100644 index 0000000..2c81536 --- /dev/null +++ b/src/foundation/api/revel/.travis.yml @@ -0,0 +1,67 @@ +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 diff --git a/src/foundation/api/revel/AUTHORS b/src/foundation/api/revel/AUTHORS new file mode 100644 index 0000000..d265544 --- /dev/null +++ b/src/foundation/api/revel/AUTHORS @@ -0,0 +1 @@ +# TODO Revel Framework Authors Information diff --git a/src/foundation/api/revel/CHANGELOG.md b/src/foundation/api/revel/CHANGELOG.md new file mode 100644 index 0000000..5273460 --- /dev/null +++ b/src/foundation/api/revel/CHANGELOG.md @@ -0,0 +1,623 @@ +# 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 + + + diff --git a/src/foundation/api/revel/CONTRIBUTING.md b/src/foundation/api/revel/CONTRIBUTING.md new file mode 100644 index 0000000..5f699ca --- /dev/null +++ b/src/foundation/api/revel/CONTRIBUTING.md @@ -0,0 +1,162 @@ +## 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 diff --git a/src/foundation/api/revel/LICENSE b/src/foundation/api/revel/LICENSE new file mode 100644 index 0000000..92246e3 --- /dev/null +++ b/src/foundation/api/revel/LICENSE @@ -0,0 +1,20 @@ +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. diff --git a/src/foundation/api/revel/README.md b/src/foundation/api/revel/README.md new file mode 100644 index 0000000..c89e193 --- /dev/null +++ b/src/foundation/api/revel/README.md @@ -0,0 +1,56 @@ +# Revel Framework + +[![Build Status](https://secure.travis-ci.org/revel/revel.svg?branch=master)](http://travis-ci.org/revel/revel) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/revel/revel)](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/images/0)](https://sourcerer.io/fame/notzippy/revel/revel/links/0) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/1)](https://sourcerer.io/fame/notzippy/revel/revel/links/1) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/2)](https://sourcerer.io/fame/notzippy/revel/revel/links/2) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/3)](https://sourcerer.io/fame/notzippy/revel/revel/links/3) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/4)](https://sourcerer.io/fame/notzippy/revel/revel/links/4) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/5)](https://sourcerer.io/fame/notzippy/revel/revel/links/5) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/6)](https://sourcerer.io/fame/notzippy/revel/revel/links/6) +[![](https://sourcerer.io/fame/notzippy/revel/revel/images/7)](https://sourcerer.io/fame/notzippy/revel/revel/links/7) diff --git a/src/foundation/api/revel/before_after_filter.go b/src/foundation/api/revel/before_after_filter.go new file mode 100644 index 0000000..e9af1d3 --- /dev/null +++ b/src/foundation/api/revel/before_after_filter.go @@ -0,0 +1,60 @@ +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 +} diff --git a/src/foundation/api/revel/binder.go b/src/foundation/api/revel/binder.go new file mode 100644 index 0000000..a42ed21 --- /dev/null +++ b/src/foundation/api/revel/binder.go @@ -0,0 +1,543 @@ +// 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) + }) +} diff --git a/src/foundation/api/revel/binder_test.go b/src/foundation/api/revel/binder_test.go new file mode 100644 index 0000000..5ffd5cd --- /dev/null +++ b/src/foundation/api/revel/binder_test.go @@ -0,0 +1,419 @@ +// 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") +} diff --git a/src/foundation/api/revel/cache/cache.go b/src/foundation/api/revel/cache/cache.go new file mode 100644 index 0000000..7406004 --- /dev/null +++ b/src/foundation/api/revel/cache/cache.go @@ -0,0 +1,145 @@ +// 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) +} diff --git a/src/foundation/api/revel/cache/cache_test.go b/src/foundation/api/revel/cache/cache_test.go new file mode 100644 index 0000000..bc54617 --- /dev/null +++ b/src/foundation/api/revel/cache/cache_test.go @@ -0,0 +1,253 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/cache/init.go b/src/foundation/api/revel/cache/init.go new file mode 100644 index 0000000..0008a6a --- /dev/null +++ b/src/foundation/api/revel/cache/init.go @@ -0,0 +1,60 @@ +// 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) + }) +} diff --git a/src/foundation/api/revel/cache/inmemory.go b/src/foundation/api/revel/cache/inmemory.go new file mode 100644 index 0000000..8257f97 --- /dev/null +++ b/src/foundation/api/revel/cache/inmemory.go @@ -0,0 +1,163 @@ +// 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 +} diff --git a/src/foundation/api/revel/cache/inmemory_test.go b/src/foundation/api/revel/cache/inmemory_test.go new file mode 100644 index 0000000..1f9cf1f --- /dev/null +++ b/src/foundation/api/revel/cache/inmemory_test.go @@ -0,0 +1,44 @@ +// 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) +} diff --git a/src/foundation/api/revel/cache/memcached.go b/src/foundation/api/revel/cache/memcached.go new file mode 100644 index 0000000..fbc7ece --- /dev/null +++ b/src/foundation/api/revel/cache/memcached.go @@ -0,0 +1,118 @@ +// 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 +} diff --git a/src/foundation/api/revel/cache/memcached_test.go b/src/foundation/api/revel/cache/memcached_test.go new file mode 100644 index 0000000..75376e0 --- /dev/null +++ b/src/foundation/api/revel/cache/memcached_test.go @@ -0,0 +1,56 @@ +// 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) +} diff --git a/src/foundation/api/revel/cache/redis.go b/src/foundation/api/revel/cache/redis.go new file mode 100644 index 0000000..62aafc4 --- /dev/null +++ b/src/foundation/api/revel/cache/redis.go @@ -0,0 +1,273 @@ +// 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) +} diff --git a/src/foundation/api/revel/cache/redis_test.go b/src/foundation/api/revel/cache/redis_test.go new file mode 100644 index 0000000..ad0b006 --- /dev/null +++ b/src/foundation/api/revel/cache/redis_test.go @@ -0,0 +1,66 @@ +// 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) +} diff --git a/src/foundation/api/revel/cache/serialization.go b/src/foundation/api/revel/cache/serialization.go new file mode 100644 index 0000000..de7ab65 --- /dev/null +++ b/src/foundation/api/revel/cache/serialization.go @@ -0,0 +1,78 @@ +// 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 +} diff --git a/src/foundation/api/revel/cache/serialization_test.go b/src/foundation/api/revel/cache/serialization_test.go new file mode 100644 index 0000000..a2bb6e9 --- /dev/null +++ b/src/foundation/api/revel/cache/serialization_test.go @@ -0,0 +1,87 @@ +// 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 +} diff --git a/src/foundation/api/revel/compress.go b/src/foundation/api/revel/compress.go new file mode 100644 index 0000000..a44053d --- /dev/null +++ b/src/foundation/api/revel/compress.go @@ -0,0 +1,395 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/compress_test.go b/src/foundation/api/revel/compress_test.go new file mode 100644 index 0000000..8b8b2fb --- /dev/null +++ b/src/foundation/api/revel/compress_test.go @@ -0,0 +1,61 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/conf/mime-types.conf b/src/foundation/api/revel/conf/mime-types.conf new file mode 100644 index 0000000..43b0235 --- /dev/null +++ b/src/foundation/api/revel/conf/mime-types.conf @@ -0,0 +1,545 @@ +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 diff --git a/src/foundation/api/revel/controller.go b/src/foundation/api/revel/controller.go new file mode 100644 index 0000000..29dbf7c --- /dev/null +++ b/src/foundation/api/revel/controller.go @@ -0,0 +1,546 @@ +// 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()) +} diff --git a/src/foundation/api/revel/controller_type.go b/src/foundation/api/revel/controller_type.go new file mode 100644 index 0000000..4ba2f33 --- /dev/null +++ b/src/foundation/api/revel/controller_type.go @@ -0,0 +1,164 @@ +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...)) +} diff --git a/src/foundation/api/revel/errors.go b/src/foundation/api/revel/errors.go new file mode 100644 index 0000000..dc3807a --- /dev/null +++ b/src/foundation/api/revel/errors.go @@ -0,0 +1,155 @@ +// 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 = "" + e.Path + ":" + strconv.Itoa(e.Line) + "" +} + +// 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, "" +} diff --git a/src/foundation/api/revel/event.go b/src/foundation/api/revel/event.go new file mode 100644 index 0000000..9c66fb0 --- /dev/null +++ b/src/foundation/api/revel/event.go @@ -0,0 +1,57 @@ +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 +} diff --git a/src/foundation/api/revel/event_test.go b/src/foundation/api/revel/event_test.go new file mode 100644 index 0000000..0baadc0 --- /dev/null +++ b/src/foundation/api/revel/event_test.go @@ -0,0 +1,24 @@ +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") +} diff --git a/src/foundation/api/revel/fakeapp_test.go b/src/foundation/api/revel/fakeapp_test.go new file mode 100644 index 0000000..a2bcabd --- /dev/null +++ b/src/foundation/api/revel/fakeapp_test.go @@ -0,0 +1,152 @@ +// 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) +} diff --git a/src/foundation/api/revel/field.go b/src/foundation/api/revel/field.go new file mode 100644 index 0000000..93f2b1c --- /dev/null +++ b/src/foundation/api/revel/field.go @@ -0,0 +1,108 @@ +// 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 +} diff --git a/src/foundation/api/revel/filter.go b/src/foundation/api/revel/filter.go new file mode 100644 index 0000000..2226dff --- /dev/null +++ b/src/foundation/api/revel/filter.go @@ -0,0 +1,31 @@ +// 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} +) diff --git a/src/foundation/api/revel/filterconfig.go b/src/foundation/api/revel/filterconfig.go new file mode 100644 index 0000000..010d7ea --- /dev/null +++ b/src/foundation/api/revel/filterconfig.go @@ -0,0 +1,223 @@ +// 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 +} diff --git a/src/foundation/api/revel/filterconfig_test.go b/src/foundation/api/revel/filterconfig_test.go new file mode 100644 index 0000000..a3973f9 --- /dev/null +++ b/src/foundation/api/revel/filterconfig_test.go @@ -0,0 +1,141 @@ +// 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) +} diff --git a/src/foundation/api/revel/flash.go b/src/foundation/api/revel/flash.go new file mode 100644 index 0000000..b27f5e0 --- /dev/null +++ b/src/foundation/api/revel/flash.go @@ -0,0 +1,78 @@ +// 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 +} diff --git a/src/foundation/api/revel/http.go b/src/foundation/api/revel/http.go new file mode 100644 index 0000000..dbdd58d --- /dev/null +++ b/src/foundation/api/revel/http.go @@ -0,0 +1,489 @@ +// 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 +} diff --git a/src/foundation/api/revel/i18n.go b/src/foundation/api/revel/i18n.go new file mode 100644 index 0000000..13fe810 --- /dev/null +++ b/src/foundation/api/revel/i18n.go @@ -0,0 +1,245 @@ +// 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, "" +} diff --git a/src/foundation/api/revel/i18n_test.go b/src/foundation/api/revel/i18n_test.go new file mode 100644 index 0000000..ce2f675 --- /dev/null +++ b/src/foundation/api/revel/i18n_test.go @@ -0,0 +1,273 @@ +// 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", ""); 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("")); message != "My name is " { + 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 +} diff --git a/src/foundation/api/revel/intercept.go b/src/foundation/api/revel/intercept.go new file mode 100644 index 0000000..2ee8677 --- /dev/null +++ b/src/foundation/api/revel/intercept.go @@ -0,0 +1,249 @@ +// 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{} +} diff --git a/src/foundation/api/revel/intercept_test.go b/src/foundation/api/revel/intercept_test.go new file mode 100644 index 0000000..ee2138d --- /dev/null +++ b/src/foundation/api/revel/intercept_test.go @@ -0,0 +1,85 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/invoker.go b/src/foundation/api/revel/invoker.go new file mode 100644 index 0000000..24d40fa --- /dev/null +++ b/src/foundation/api/revel/invoker.go @@ -0,0 +1,50 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/invoker_test.go b/src/foundation/api/revel/invoker_test.go new file mode 100644 index 0000000..dff94cc --- /dev/null +++ b/src/foundation/api/revel/invoker_test.go @@ -0,0 +1,134 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/libs.go b/src/foundation/api/revel/libs.go new file mode 100644 index 0000000..fb17178 --- /dev/null +++ b/src/foundation/api/revel/libs.go @@ -0,0 +1,86 @@ +// 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 +} diff --git a/src/foundation/api/revel/libs_test.go b/src/foundation/api/revel/libs_test.go new file mode 100644 index 0000000..d8f8355 --- /dev/null +++ b/src/foundation/api/revel/libs_test.go @@ -0,0 +1,38 @@ +// 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'") + } +} diff --git a/src/foundation/api/revel/logger.go b/src/foundation/api/revel/logger.go new file mode 100644 index 0000000..7e928f2 --- /dev/null +++ b/src/foundation/api/revel/logger.go @@ -0,0 +1,65 @@ +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) + } +} diff --git a/src/foundation/api/revel/logger/composite_multihandler.go b/src/foundation/api/revel/logger/composite_multihandler.go new file mode 100644 index 0000000..fb15e8a --- /dev/null +++ b/src/foundation/api/revel/logger/composite_multihandler.go @@ -0,0 +1,174 @@ +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 + } + } +} diff --git a/src/foundation/api/revel/logger/doc.go b/src/foundation/api/revel/logger/doc.go new file mode 100644 index 0000000..0f6156a --- /dev/null +++ b/src/foundation/api/revel/logger/doc.go @@ -0,0 +1,15 @@ +/* + 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 diff --git a/src/foundation/api/revel/logger/handlers.go b/src/foundation/api/revel/logger/handlers.go new file mode 100644 index 0000000..dee850e --- /dev/null +++ b/src/foundation/api/revel/logger/handlers.go @@ -0,0 +1,210 @@ +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:]...) + } + } + } +} diff --git a/src/foundation/api/revel/logger/init.go b/src/foundation/api/revel/logger/init.go new file mode 100644 index 0000000..6a02cf8 --- /dev/null +++ b/src/foundation/api/revel/logger/init.go @@ -0,0 +1,189 @@ +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 +} diff --git a/src/foundation/api/revel/logger/init_test.go b/src/foundation/api/revel/logger/init_test.go new file mode 100644 index 0000000..50fcd61 --- /dev/null +++ b/src/foundation/api/revel/logger/init_test.go @@ -0,0 +1,273 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/logger/log_function_map.go b/src/foundation/api/revel/logger/log_function_map.go new file mode 100644 index 0000000..ebbc76d --- /dev/null +++ b/src/foundation/api/revel/logger/log_function_map.go @@ -0,0 +1,37 @@ +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) + }, +} diff --git a/src/foundation/api/revel/logger/logger.go b/src/foundation/api/revel/logger/logger.go new file mode 100644 index 0000000..b9abdaf --- /dev/null +++ b/src/foundation/api/revel/logger/logger.go @@ -0,0 +1,203 @@ +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 +} diff --git a/src/foundation/api/revel/logger/revel_logger.go b/src/foundation/api/revel/logger/revel_logger.go new file mode 100644 index 0000000..b3a6f86 --- /dev/null +++ b/src/foundation/api/revel/logger/revel_logger.go @@ -0,0 +1,142 @@ +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 +} diff --git a/src/foundation/api/revel/logger/terminal_format.go b/src/foundation/api/revel/logger/terminal_format.go new file mode 100644 index 0000000..ca2cd15 --- /dev/null +++ b/src/foundation/api/revel/logger/terminal_format.go @@ -0,0 +1,245 @@ +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) + } +} diff --git a/src/foundation/api/revel/logger/utils.go b/src/foundation/api/revel/logger/utils.go new file mode 100644 index 0000000..3918cc1 --- /dev/null +++ b/src/foundation/api/revel/logger/utils.go @@ -0,0 +1,110 @@ +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() +} diff --git a/src/foundation/api/revel/logger/wrap_handlers.go b/src/foundation/api/revel/logger/wrap_handlers.go new file mode 100644 index 0000000..3d68e75 --- /dev/null +++ b/src/foundation/api/revel/logger/wrap_handlers.go @@ -0,0 +1,98 @@ +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 + } +} diff --git a/src/foundation/api/revel/model/revel_container.go b/src/foundation/api/revel/model/revel_container.go new file mode 100644 index 0000000..c3398d9 --- /dev/null +++ b/src/foundation/api/revel/model/revel_container.go @@ -0,0 +1,14 @@ +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 + } +} diff --git a/src/foundation/api/revel/module.go b/src/foundation/api/revel/module.go new file mode 100644 index 0000000..e51ac76 --- /dev/null +++ b/src/foundation/api/revel/module.go @@ -0,0 +1,213 @@ +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.???. + 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) + } +} diff --git a/src/foundation/api/revel/namespace.go b/src/foundation/api/revel/namespace.go new file mode 100644 index 0000000..9f66cf4 --- /dev/null +++ b/src/foundation/api/revel/namespace.go @@ -0,0 +1,37 @@ +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() +} diff --git a/src/foundation/api/revel/panic.go b/src/foundation/api/revel/panic.go new file mode 100644 index 0000000..464f2de --- /dev/null +++ b/src/foundation/api/revel/panic.go @@ -0,0 +1,50 @@ +// 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) +} diff --git a/src/foundation/api/revel/params.go b/src/foundation/api/revel/params.go new file mode 100644 index 0000000..3df3dde --- /dev/null +++ b/src/foundation/api/revel/params.go @@ -0,0 +1,180 @@ +// 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:]) +} diff --git a/src/foundation/api/revel/params_test.go b/src/foundation/api/revel/params_test.go new file mode 100644 index 0000000..571a244 --- /dev/null +++ b/src/foundation/api/revel/params_test.go @@ -0,0 +1,182 @@ +// 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 +} diff --git a/src/foundation/api/revel/results.go b/src/foundation/api/revel/results.go new file mode 100644 index 0000000..59d0507 --- /dev/null +++ b/src/foundation/api/revel/results.go @@ -0,0 +1,513 @@ +// 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
 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 
 tags.
+		tl := strings.ToLower(text)
+		if strings.Contains(tl, "
") {
+			insidePre = true
+		}
+		// Trim if not inside a 
 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, "
") { + 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()) +} diff --git a/src/foundation/api/revel/results_test.go b/src/foundation/api/revel/results_test.go new file mode 100644 index 0000000..ce375d1 --- /dev/null +++ b/src/foundation/api/revel/results_test.go @@ -0,0 +1,73 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/revel.go b/src/foundation/api/revel/revel.go new file mode 100644 index 0000000..27ac6ef --- /dev/null +++ b/src/foundation/api/revel/revel.go @@ -0,0 +1,301 @@ +// 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 +} diff --git a/src/foundation/api/revel/revel_hooks.go b/src/foundation/api/revel/revel_hooks.go new file mode 100644 index 0000000..9378166 --- /dev/null +++ b/src/foundation/api/revel/revel_hooks.go @@ -0,0 +1,103 @@ +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...) +} diff --git a/src/foundation/api/revel/revel_test.go b/src/foundation/api/revel/revel_test.go new file mode 100644 index 0000000..1f8c87a --- /dev/null +++ b/src/foundation/api/revel/revel_test.go @@ -0,0 +1,13 @@ +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 +} diff --git a/src/foundation/api/revel/router.go b/src/foundation/api/revel/router.go new file mode 100644 index 0000000..54fb6e2 --- /dev/null +++ b/src/foundation/api/revel/router.go @@ -0,0 +1,846 @@ +// 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 = "" + 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) + } + }) +} diff --git a/src/foundation/api/revel/router_test.go b/src/foundation/api/revel/router_test.go new file mode 100644 index 0000000..fe66970 --- /dev/null +++ b/src/foundation/api/revel/router_test.go @@ -0,0 +1,678 @@ +// 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 +} diff --git a/src/foundation/api/revel/server-engine.go b/src/foundation/api/revel/server-engine.go new file mode 100644 index 0000000..0ac4b8a --- /dev/null +++ b/src/foundation/api/revel/server-engine.go @@ -0,0 +1,229 @@ +// 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 +} diff --git a/src/foundation/api/revel/server.go b/src/foundation/api/revel/server.go new file mode 100644 index 0000000..0f5c786 --- /dev/null +++ b/src/foundation/api/revel/server.go @@ -0,0 +1,153 @@ +// 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 diff --git a/src/foundation/api/revel/server_adapter_go.go b/src/foundation/api/revel/server_adapter_go.go new file mode 100644 index 0000000..bacaf91 --- /dev/null +++ b/src/foundation/api/revel/server_adapter_go.go @@ -0,0 +1,647 @@ +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) +} diff --git a/src/foundation/api/revel/server_test.go b/src/foundation/api/revel/server_test.go new file mode 100644 index 0000000..e37fa24 --- /dev/null +++ b/src/foundation/api/revel/server_test.go @@ -0,0 +1,148 @@ +// 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) +) diff --git a/src/foundation/api/revel/session/init.go b/src/foundation/api/revel/session/init.go new file mode 100644 index 0000000..56069e5 --- /dev/null +++ b/src/foundation/api/revel/session/init.go @@ -0,0 +1,10 @@ +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") +} diff --git a/src/foundation/api/revel/session/session.go b/src/foundation/api/revel/session/session.go new file mode 100644 index 0000000..c5696bc --- /dev/null +++ b/src/foundation/api/revel/session/session.go @@ -0,0 +1,364 @@ +// 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 +} diff --git a/src/foundation/api/revel/session/session_cookie_test.go b/src/foundation/api/revel/session/session_cookie_test.go new file mode 100644 index 0000000..ca6c829 --- /dev/null +++ b/src/foundation/api/revel/session/session_cookie_test.go @@ -0,0 +1,76 @@ +// 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) + } +} diff --git a/src/foundation/api/revel/session/session_test.go b/src/foundation/api/revel/session/session_test.go new file mode 100644 index 0000000..6be1ce5 --- /dev/null +++ b/src/foundation/api/revel/session/session_test.go @@ -0,0 +1,64 @@ +// 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) + +} diff --git a/src/foundation/api/revel/session_adapter_cookie.go b/src/foundation/api/revel/session_adapter_cookie.go new file mode 100644 index 0000000..ebe871d --- /dev/null +++ b/src/foundation/api/revel/session_adapter_cookie.go @@ -0,0 +1,144 @@ +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 + +} diff --git a/src/foundation/api/revel/session_engine.go b/src/foundation/api/revel/session_engine.go new file mode 100644 index 0000000..1f95d7b --- /dev/null +++ b/src/foundation/api/revel/session_engine.go @@ -0,0 +1,35 @@ +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"]() + } +} diff --git a/src/foundation/api/revel/session_filter.go b/src/foundation/api/revel/session_filter.go new file mode 100644 index 0000000..f10470a --- /dev/null +++ b/src/foundation/api/revel/session_filter.go @@ -0,0 +1,25 @@ +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) + } +} diff --git a/src/foundation/api/revel/template.go b/src/foundation/api/revel/template.go new file mode 100644 index 0000000..c5bd91e --- /dev/null +++ b/src/foundation/api/revel/template.go @@ -0,0 +1,477 @@ +// 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} +} diff --git a/src/foundation/api/revel/template_adapter_go.go b/src/foundation/api/revel/template_adapter_go.go new file mode 100644 index 0000000..4111307 --- /dev/null +++ b/src/foundation/api/revel/template_adapter_go.go @@ -0,0 +1,129 @@ +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) + } +} diff --git a/src/foundation/api/revel/template_engine.go b/src/foundation/api/revel/template_engine.go new file mode 100644 index 0000000..a62f5f2 --- /dev/null +++ b/src/foundation/api/revel/template_engine.go @@ -0,0 +1,118 @@ +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 +} diff --git a/src/foundation/api/revel/template_functions.go b/src/foundation/api/revel/template_functions.go new file mode 100644 index 0000000..98bb988 --- /dev/null +++ b/src/foundation/api/revel/template_functions.go @@ -0,0 +1,341 @@ +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(``, + 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(``, + 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(``, + 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
+ "nl2br": func(text string) template.HTML { + return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -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) +} diff --git a/src/foundation/api/revel/templates/errors/403.html b/src/foundation/api/revel/templates/errors/403.html new file mode 100644 index 0000000..23e01a2 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/403.html @@ -0,0 +1,16 @@ + + + + Forbidden + + + {{with .Error}} +

+ {{.Title}} +

+

+ {{.Description}} +

+ {{end}} + + diff --git a/src/foundation/api/revel/templates/errors/403.json b/src/foundation/api/revel/templates/errors/403.json new file mode 100644 index 0000000..13e1d15 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/403.json @@ -0,0 +1,4 @@ +{ + "title": "{{js .Error.Title}}", + "description": "{{js .Error.Description}}" +} diff --git a/src/foundation/api/revel/templates/errors/403.txt b/src/foundation/api/revel/templates/errors/403.txt new file mode 100644 index 0000000..cbfc955 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/403.txt @@ -0,0 +1,3 @@ +{{.Error.Title}} + +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/403.xml b/src/foundation/api/revel/templates/errors/403.xml new file mode 100644 index 0000000..0696e50 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/403.xml @@ -0,0 +1 @@ +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/404-dev.html b/src/foundation/api/revel/templates/errors/404-dev.html new file mode 100644 index 0000000..e5f0e06 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/404-dev.html @@ -0,0 +1,63 @@ + + + +
+

These routes have been tried, in this order :

+
    + {{range .Router.Routes}} +
  1. {{pad .Method 10}}{{pad .Path 50}}{{.Action}} {{with .ModuleSource}}(Route Module:{{.Name}}){{end}}
  2. + {{end}} +
+
diff --git a/src/foundation/api/revel/templates/errors/404.html b/src/foundation/api/revel/templates/errors/404.html new file mode 100644 index 0000000..f548ebb --- /dev/null +++ b/src/foundation/api/revel/templates/errors/404.html @@ -0,0 +1,26 @@ + + + + Not found + + + +{{if .DevMode}} + +{{template "errors/404-dev.html" .}} + +{{else}} + + {{with .Error}} +

+ {{.Title}} +

+

+ {{.Description}} +

+ {{end}} + +{{end}} + + + diff --git a/src/foundation/api/revel/templates/errors/404.json b/src/foundation/api/revel/templates/errors/404.json new file mode 100644 index 0000000..13e1d15 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/404.json @@ -0,0 +1,4 @@ +{ + "title": "{{js .Error.Title}}", + "description": "{{js .Error.Description}}" +} diff --git a/src/foundation/api/revel/templates/errors/404.txt b/src/foundation/api/revel/templates/errors/404.txt new file mode 100644 index 0000000..cbfc955 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/404.txt @@ -0,0 +1,3 @@ +{{.Error.Title}} + +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/404.xml b/src/foundation/api/revel/templates/errors/404.xml new file mode 100644 index 0000000..29839a5 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/404.xml @@ -0,0 +1 @@ +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/405.html b/src/foundation/api/revel/templates/errors/405.html new file mode 100644 index 0000000..58f19bc --- /dev/null +++ b/src/foundation/api/revel/templates/errors/405.html @@ -0,0 +1,16 @@ + + + + Method not allowed + + + {{with .Error}} +

+ {{.Title}} +

+

+ {{.Description}} +

+ {{end}} + + diff --git a/src/foundation/api/revel/templates/errors/405.json b/src/foundation/api/revel/templates/errors/405.json new file mode 100644 index 0000000..13e1d15 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/405.json @@ -0,0 +1,4 @@ +{ + "title": "{{js .Error.Title}}", + "description": "{{js .Error.Description}}" +} diff --git a/src/foundation/api/revel/templates/errors/405.txt b/src/foundation/api/revel/templates/errors/405.txt new file mode 100644 index 0000000..cbfc955 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/405.txt @@ -0,0 +1,3 @@ +{{.Error.Title}} + +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/405.xml b/src/foundation/api/revel/templates/errors/405.xml new file mode 100644 index 0000000..c5bea69 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/405.xml @@ -0,0 +1 @@ +{{.Error.Description}} diff --git a/src/foundation/api/revel/templates/errors/500-dev.html b/src/foundation/api/revel/templates/errors/500-dev.html new file mode 100644 index 0000000..4f0d46c --- /dev/null +++ b/src/foundation/api/revel/templates/errors/500-dev.html @@ -0,0 +1,133 @@ + + {{with .Error}} + + {{if .Path}} +
+

In {{.Path}} + {{if .Line}} + (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}}) + {{end}} +

+ {{range .ContextSource}} +
+ {{.Line}}: +
{{.Source}}
+
+ {{end}} +
+ {{end}} + {{if .Stack}} +
+

Call Stack

+ {{.Stack}} +
+ {{end}} + {{if .MetaError}} +
+

Additionally, an error occurred while handling this error.

+
+ {{.MetaError}} +
+
+ {{end}} + {{end}} diff --git a/src/foundation/api/revel/templates/errors/500.html b/src/foundation/api/revel/templates/errors/500.html new file mode 100644 index 0000000..ad63527 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/500.html @@ -0,0 +1,16 @@ + + + + Application error + + + {{if .DevMode}} + {{template "errors/500-dev.html" .}} + {{else}} +

Oops, an error occured

+

+ This exception has been logged. +

+ {{end}} + + diff --git a/src/foundation/api/revel/templates/errors/500.json b/src/foundation/api/revel/templates/errors/500.json new file mode 100644 index 0000000..13e1d15 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/500.json @@ -0,0 +1,4 @@ +{ + "title": "{{js .Error.Title}}", + "description": "{{js .Error.Description}}" +} diff --git a/src/foundation/api/revel/templates/errors/500.txt b/src/foundation/api/revel/templates/errors/500.txt new file mode 100644 index 0000000..dde13f4 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/500.txt @@ -0,0 +1,15 @@ +{{.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}} diff --git a/src/foundation/api/revel/templates/errors/500.xml b/src/foundation/api/revel/templates/errors/500.xml new file mode 100644 index 0000000..444b4f8 --- /dev/null +++ b/src/foundation/api/revel/templates/errors/500.xml @@ -0,0 +1,4 @@ + + {{.Error.Title}} + {{.Error.Description}} + diff --git a/src/foundation/api/revel/testdata/app/views/footer.html b/src/foundation/api/revel/testdata/app/views/footer.html new file mode 100644 index 0000000..9307824 --- /dev/null +++ b/src/foundation/api/revel/testdata/app/views/footer.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/foundation/api/revel/testdata/app/views/header.html b/src/foundation/api/revel/testdata/app/views/header.html new file mode 100644 index 0000000..583bb34 --- /dev/null +++ b/src/foundation/api/revel/testdata/app/views/header.html @@ -0,0 +1,45 @@ + + + + + {{.title}} + + + {{range .moreStyles}} + + {{end}} + + + {{range .moreScripts}} + + {{end}} + + + + + +
+ {{if .flash.error}} +

+ {{.flash.error}} +

+ {{end}} + {{if .flash.success}} +

+ {{.flash.success}} +

+ {{end}} + diff --git a/src/foundation/api/revel/testdata/app/views/hotels/show.html b/src/foundation/api/revel/testdata/app/views/hotels/show.html new file mode 100644 index 0000000..6d10898 --- /dev/null +++ b/src/foundation/api/revel/testdata/app/views/hotels/show.html @@ -0,0 +1,37 @@ +{{template "header.html" .}} + +

View hotel

+ +{{with .hotel}} +
+ +

+ Name: {{.Name}} +

+

+ Address: {{.Address}} +

+

+ City: {{.City}} +

+

+ State: {{.State}} +

+

+ Zip: {{.Zip}} +

+

+ Country: {{.Country}} +

+

+ Nightly rate: {{.Price}} +

+ +

+ + Back to search +

+
+{{end}} + +{{template "footer.html" .}} diff --git a/src/foundation/api/revel/testdata/conf/app.conf b/src/foundation/api/revel/testdata/conf/app.conf new file mode 100644 index 0000000..6efa0cf --- /dev/null +++ b/src/foundation/api/revel/testdata/conf/app.conf @@ -0,0 +1,44 @@ +# 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 diff --git a/src/foundation/api/revel/testdata/conf/routes b/src/foundation/api/revel/testdata/conf/routes new file mode 100644 index 0000000..ec40efd --- /dev/null +++ b/src/foundation/api/revel/testdata/conf/routes @@ -0,0 +1,16 @@ +# 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 diff --git a/src/foundation/api/revel/testdata/i18n/config/test_app.conf b/src/foundation/api/revel/testdata/i18n/config/test_app.conf new file mode 100644 index 0000000..40e12b6 --- /dev/null +++ b/src/foundation/api/revel/testdata/i18n/config/test_app.conf @@ -0,0 +1,33 @@ +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 diff --git a/src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl b/src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl new file mode 100644 index 0000000..2e75b47 --- /dev/null +++ b/src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl @@ -0,0 +1,9 @@ +greeting=Hallo +greeting.name=Rob +greeting.suffix=, welkom bij Revel! + +[NL] +greeting=Goeiedag + +[BE] +greeting=Hallokes diff --git a/src/foundation/api/revel/testdata/i18n/messages/english_messages.en b/src/foundation/api/revel/testdata/i18n/messages/english_messages.en new file mode 100644 index 0000000..653bfd9 --- /dev/null +++ b/src/foundation/api/revel/testdata/i18n/messages/english_messages.en @@ -0,0 +1,21 @@ +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 diff --git a/src/foundation/api/revel/testdata/i18n/messages/english_messages2.en b/src/foundation/api/revel/testdata/i18n/messages/english_messages2.en new file mode 100644 index 0000000..ea74539 --- /dev/null +++ b/src/foundation/api/revel/testdata/i18n/messages/english_messages2.en @@ -0,0 +1 @@ +greeting2=Yo! diff --git a/src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt b/src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/foundation/api/revel/testdata/public/js/sessvars.js b/src/foundation/api/revel/testdata/public/js/sessvars.js new file mode 100644 index 0000000..9eaafea --- /dev/null +++ b/src/foundation/api/revel/testdata/public/js/sessvars.js @@ -0,0 +1 @@ +console.log('Test file'); diff --git a/src/foundation/api/revel/testing/testsuite.go b/src/foundation/api/revel/testing/testsuite.go new file mode 100644 index 0000000..ff02472 --- /dev/null +++ b/src/foundation/api/revel/testing/testsuite.go @@ -0,0 +1,411 @@ +// 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) +} diff --git a/src/foundation/api/revel/testing/testsuite_test.go b/src/foundation/api/revel/testing/testsuite_test.go new file mode 100644 index 0000000..e82066c --- /dev/null +++ b/src/foundation/api/revel/testing/testsuite_test.go @@ -0,0 +1,303 @@ +// 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, + } + } +} diff --git a/src/foundation/api/revel/util.go b/src/foundation/api/revel/util.go new file mode 100644 index 0000000..340c599 --- /dev/null +++ b/src/foundation/api/revel/util.go @@ -0,0 +1,280 @@ +// 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) +} diff --git a/src/foundation/api/revel/util_test.go b/src/foundation/api/revel/util_test.go new file mode 100644 index 0000000..96f19ef --- /dev/null +++ b/src/foundation/api/revel/util_test.go @@ -0,0 +1,95 @@ +// 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) +} diff --git a/src/foundation/api/revel/utils/simplestack.go b/src/foundation/api/revel/utils/simplestack.go new file mode 100644 index 0000000..bb22430 --- /dev/null +++ b/src/foundation/api/revel/utils/simplestack.go @@ -0,0 +1,103 @@ +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) +} diff --git a/src/foundation/api/revel/utils/simplestack_test.go b/src/foundation/api/revel/utils/simplestack_test.go new file mode 100644 index 0000000..3a93740 --- /dev/null +++ b/src/foundation/api/revel/utils/simplestack_test.go @@ -0,0 +1,127 @@ +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() + } + } + }) +} diff --git a/src/foundation/api/revel/validation.go b/src/foundation/api/revel/validation.go new file mode 100644 index 0000000..49cf3a9 --- /dev/null +++ b/src/foundation/api/revel/validation.go @@ -0,0 +1,334 @@ +// 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 diff --git a/src/foundation/api/revel/validation_test.go b/src/foundation/api/revel/validation_test.go new file mode 100644 index 0000000..a3a7ac7 --- /dev/null +++ b/src/foundation/api/revel/validation_test.go @@ -0,0 +1,106 @@ +// 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") + } + }) + +} diff --git a/src/foundation/api/revel/validators.go b/src/foundation/api/revel/validators.go new file mode 100644 index 0000000..31aef42 --- /dev/null +++ b/src/foundation/api/revel/validators.go @@ -0,0 +1,633 @@ +// 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(/*\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") +} diff --git a/src/foundation/api/revel/validators_test.go b/src/foundation/api/revel/validators_test.go new file mode 100644 index 0000000..9baf177 --- /dev/null +++ b/src/foundation/api/revel/validators_test.go @@ -0,0 +1,640 @@ +// 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--.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{ + `qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false, + `a\r\nbqd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false, + `FooBar`: false, + `Foo<12>Bar`: true, + `Foo<>Bar`: true, + `Foo
Bar`: false, + `Foo Baz`: false, + `I <3 Ponies!`: true, + `I like Golang\t\n`: true, + `I & like Golang\t\n`: false, + ` `: 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/>qwdojiqwdqwdqwdoijqwdoiqjd`: true, + `abcd/>qwdqwdoijhwer/>qwdojiqwdqwdqwdoijqwdoiqjd`: 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{ + `qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false, + `a\r\nbqd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false, + `FooBar`: false, + `Foo<12>Bar`: true, + `Foo<>Bar`: true, + `Foo
Bar`: false, + `Foo Baz`: false, + `I <3 Ponies!`: true, + `I like Golang\t\n`: true, + `I & like Golang\t\n`: false, + ` `: 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/>qwdojiqwdqwdqwdoijqwdoiqjd`: true, + `abcd/>qwdqwdoijhwer/>qwdojiqwdqwdqwdoijqwdoiqjd`: 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, + `../../qwdqwdqwd/../qwdqwdqwd.txt`: false, + `../../qwdqwdqwd/../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, + `../../qwdqwdqwd/../qwdqwdqwd.txt`: false, + `../../qwdqwdqwd/../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) + } +} diff --git a/src/foundation/api/revel/version.go b/src/foundation/api/revel/version.go new file mode 100644 index 0000000..6d666ca --- /dev/null +++ b/src/foundation/api/revel/version.go @@ -0,0 +1,16 @@ +// 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" +) diff --git a/src/foundation/api/revel/watcher.go b/src/foundation/api/revel/watcher.go new file mode 100644 index 0000000..0dfc18c --- /dev/null +++ b/src/foundation/api/revel/watcher.go @@ -0,0 +1,299 @@ +// 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:]) +} -- 2.16.6