diff --git a/README.md b/README.md index bd0557a..2eda202 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Any controller function also has access to the the FW/1 API (after `require`ing * `(cookie rc name)` - returns the value of `name` from the cookie scope. * `(cookie rc name value)` - sets `name` to `value` in the cookie scope, and returns the updated `rc`. -* `(event rc name)` - returns the value of `name` from the event scope (`:action`, `:section`, `:item, or `:config`). +* `(event rc name)` - returns the value of `name` from the event scope (`:action`, `:section`, `:item`, or `:config`). * `(event rc name value)` - sets `name` to `value` in the event scope, and returns the updated `rc`. * `(flash rc name)` - returns the value of `name` from the flash scope. * `(flash rc name value)` - sets `name` to `value` in the flash scope, and returns the updated `rc`. @@ -153,18 +153,16 @@ as a map (preferred) or as an arbitrary number of inline key / value pairs (lega * `:error` - the action - _"section.item"_ - to execute if an exception is thrown from the initial request, defaults to `:default-section` value and `"error"` _[untested]_. * `:home` - the _"section.item"_ pair used for the / URL, defaults to `:default-section` and `:default-item` values. * `:json-config` - specify formatting information for Cheshire's JSON `generate-string`, used in `render-json` (`:date-format`, `:ex`, `:key-fn`, etc). -* `:middleware` - specify additional Ring-compatible middleware to use. By default, this is prepended to the default list (params, flash, session, resource). The behavior can be changed by specifying a keyword at the start of the list of additional middleware: `:append`, `:prepend`, `:replace`. +* `:middleware-default-fn` - an optional function that will be applied to Ring's site defaults; note that by default we do **not** enable the XSRF Anti-Forgery middleware that is normally part of the site defaults since that requires session scope and client knowledge which is not appropriate for many uses of FW/1. Specify `#(assoc-in [:security :anti-forgery] true)` here to opt into XSRF Anti-Forgery (you'll probably also want to change the :session :store from the in-memory default unless you have just a single server instance). * `:options-access-control` - specify what an `OPTIONS` request should return (`:origin`, `:headers`, `:credentials`, `:max-age`). * `:password` - specify a password for the application reload URL flag, default `"secret"` - see also `:reload`. * `:reload` - specify an `rc` key for the application reload URL flag, default `:reload` - see also `:password`. * `:reload-application-on-every-request` - boolean, whether to reload controller, view and layout components on every request (intended for development of applications). * `:routes` - a vector of hash maps, specifying route patterns and what to map them to (full documentation coming in due course). * `:selmer-tags` - you can specify a map that is passed to the Selmer parser to override what characters are used to identify tags, filters -* `:session-store` - specify storage used for Ring session storage. Legal values are `:memory` and `:cookie`. Default is whatever is Ring's default (which is memory storage as of this writing). * `:suffix` - the file extension used for views and layouts. Default is `"html"`. -For example: `(fw1/configure-router :default-section "hello" :default-item "world")` will tell FW/1 to use `hello.world` as the default action. -You could also say: `(fw1/configure-router {:default-section "hello" :default-item "world"})`. +For example: `(fw1/configure-router {:default-section "hello" :default-item "world"})` will tell FW/1 to use `hello.world` as the default action. License & Copyright =================== diff --git a/build.boot b/build.boot index e117ee0..58a57b8 100644 --- a/build.boot +++ b/build.boot @@ -1,5 +1,5 @@ (def project 'framework-one) -(def version "0.7.2") +(def version "0.7.3") (task-options! pom {:project project @@ -18,6 +18,7 @@ [cheshire "5.6.3"] ; core web request handling [ring "1.6.0-beta6"] + [ring/ring-defaults "0.2.1"] ; view/layout templates [selmer "1.0.9"] ; standardized application start/stop diff --git a/src/framework/one.clj b/src/framework/one.clj index bf1e92e..30a2864 100644 --- a/src/framework/one.clj +++ b/src/framework/one.clj @@ -17,18 +17,12 @@ [clojure.data.xml :as xml] [clojure.stacktrace :as stacktrace] [clojure.string :as str] - [clojure.walk :as walk] [com.stuartsierra.component :as component] [compojure.coercions :refer :all] [compojure.core :refer :all] [compojure.route :as route] [ring.adapter.jetty :as jetty] - [ring.middleware.flash :as ring-f] - [ring.middleware.params :as ring-p] - [ring.middleware.resource :as ring-r] - [ring.middleware.session :as ring-s] - [ring.middleware.session.cookie :refer [cookie-store]] - [ring.middleware.session.memory :refer [memory-store]] + [ring.middleware.defaults :as ring-md] [selmer.filters] [selmer.parser] [selmer.util :refer [resource-path]])) @@ -59,7 +53,7 @@ ;; FW/1 base functionality - this is essentially the public API of the ;; framework with the entry point to create Ring middleware being: -;; (fw1/start) - returns Ring middleware for your application +;; (fw1/default-handler) - returns Ring middleware for your application ;; See the bottom of this file for more details (def cookie @@ -149,7 +143,7 @@ (def session "Get / set items in session scope: (session rc name) - returns the named session variable - (session rc value) - sets the named session variable + (session rc name value) - sets the named session variable Session variables persist across requests and use Ring's session middleware (and can be memory or cookie-based at the moment)." (scope-access :session)) @@ -253,8 +247,8 @@ desired layout path, attempt to render that layout." [config rc exceptional? html layout-path] (let [render-layout (fn [] (selmer.parser/render-file layout-path - (assoc rc :body [:safe html]) - (:selmer-tags config)))] + (assoc rc :body [:safe html]) + (:selmer-tags config)))] (if exceptional? ;; if we fail to render a layout while processing an exception ;; just treat the layout as not found so the original view will @@ -362,7 +356,7 @@ render views and layouts." [config section item req] (let [exceptional? (::handling-exception req) - rc (-> (walk/keywordize-keys (:params req)) + rc (-> (:params req) (pack-request req) (event :action (str section "." item)) (event :section section) @@ -395,34 +389,15 @@ {"Access-Control-Max-Age" (str (:max-age access-control))}]})) (defn- default-middleware - "The default set of Ring middleware we apply in FW/1" - [config] - [ring-p/wrap-params - ring-f/wrap-flash - (fn [h] - (condp = (:session-store config) - :memory (ring-s/wrap-session h {:store (memory-store)}) - :cookie (ring-s/wrap-session h {:store (cookie-store)}) - (ring-s/wrap-session h))) - (fn [req] (ring-r/wrap-resource req (stem config "/")))]) - -(defn- merge-middleware - "Return a function that, given any user-supplied middleware (as a vector), - will combine that will our default Ring middleware (see above). The user - supplied middleware vector is prepended before the default middleware by - default, but can have a placement as its first element: - - :append - append supplied middleware after defaults - - :replace - use supplied middleware instead of defaults - - :prepend - prepend supplied middleware before defaults" - [config] - (fn [middleware] - (if middleware - (condp = (first middleware) - :append (concat (default-middleware config) (rest middleware)) - :replace (rest middleware) - :prepend (concat (rest middleware) (default-middleware config)) - (concat middleware (default-middleware config))) - (default-middleware config)))) + "The default Ring middleware we apply in FW/1. Returns a single + composed piece of middleware. We start with Ring's site defaults + and the fn passed in may modify those defaults." + [modifier-fn] + (fn [handler] + (ring-md/wrap-defaults handler (-> ring-md/site-defaults + ; you have to explicitly opt in to this: + (assoc-in [:security :anti-forgery] false) + modifier-fn)))) (def ^:private default-options-access-control {:origin "*" @@ -440,6 +415,9 @@ :home (if (:home options) (clojure.string/split (:home options) #"\.") [(:default-section options) (:default-item options)]) + ; can modify site-defaults + :middleware (default-middleware (or (:middleware-default-fn options) + identity)) :options-access-control (merge default-options-access-control (:options-access-control options)))) @@ -452,46 +430,49 @@ :reload :reload :reload-application-on-every-request false :suffix "html" ; views / layouts would be .html - :version "0.6.0"}) + :version "0.7.3"}) (defn- build-config "Given a 'public' application configuration, return the fully built FW/1 configuration for it." [app-config] - (let [config (framework-defaults (merge default-options app-config))] - (update config :middleware (merge-middleware config)))) + (framework-defaults (merge default-options app-config))) + +(defn- handler + "The underlying Ring request handler for FW/1." + [config section item req fw1-router] + (try + (if (= :options (:request-method req)) + (render-options config req) + (render-request config section item req)) + (catch Exception e + (if (::handling-exception req) + (do + (stacktrace/print-stack-trace e) + (html-response 500 (str e))) + (let [section (first (:error config)) + item (second (:error config)) + fw1 (fw1-router (keyword section item))] + (fw1 (-> req + (assoc ::handling-exception true) + (assoc :uri (str "/" section "/" item)) + (assoc-in [:params :exception] e)))))))) (defn configure-router - "Given the application configuration, return a function that takes a + "Given the application configuration, return a router function that takes a :section/item keyword and produces a function that handles a (Ring) request." [app-config] - (let [config (build-config app-config)] - (fn configured-fw1 - ([] (configured-fw1 (keyword (first (:home config)) - (second (:home config))))) + (let [config (build-config app-config)] + (fn fw1-router + ([] (fw1-router (keyword (first (:home config)) + (second (:home config))))) ([section-item] ; :section or :section/item (let [[section item] (if (namespace section-item) [(namespace section-item) (name section-item)] - [(name section-item) (second (:home config))])] - (reduce (fn [handler middleware] (middleware handler)) - (fn [req] - (try - (if (= :options (:request-method req)) - (render-options config req) - (render-request config section item req)) - (catch Exception e - (if (::handling-exception req) - (do - (stacktrace/print-stack-trace e) - (html-response 500 (str e))) - (let [section (first (:error config)) - item (second (:error config))] - ((configured-fw1 (keyword section item)) - (-> req - (assoc ::handling-exception true) - (assoc :uri (str "/" section "/" item)) - (assoc-in [:params :exception] e)))))))) - (:middleware config))))))) + [(name section-item) (second (:home config))]) + middleware-fn (:middleware config)] + (middleware-fn (fn [req] + (handler config section item req fw1-router)))))))) (defn default-handler "Build a default FW/1 handler from the application and configuration.