unescape(str)
escape(str)
escape_pattern(str)
parse_query_string(str)
encode_query_string(tbl)
underscore(str)
slugify(str)
uniquify(tbl)
trim(str)
trim_all(tbl)
trim_filter(tbl, [{keys ...}], [empty_val=nil])
to_json(obj)
from_json(str)
time_ago_in_words(date, [parts=1], [suffix="ago"])
encode_base64(str)
decode_base64(str)
hmac_sha1(secret, str)
encode_with_secret(object, secret=config.secret)
decode_with_secret(msg_and_sig, secret=config.secret)
autoload(prefix, tbl={})
cache.cached(fn_or_tbl)
cache.delete(key, [dict_name="page_cache"])
cache.delete_all([dict_name="page_cache"])
cache.delete_path(path, [dict_name="page_cache"])
respond_to(verbs_to_fn={})
capture_errors(fn_or_tbl)
capture_errors_json(fn)
yield_error(error_message)
obj, msg, ... = assert_error(obj, msg, ...)
json_params(fn)
Utility functions are found in:
local util = require("lapis.util")
util = require "lapis.util"
unescape(str)
URL unescapes string
escape(str)
URL escapes string
escape_pattern(str)
Escapes string for use in Lua pattern
parse_query_string(str)
Parses query string into a table
encode_query_string(tbl)
Converts a key,value table into a query string
underscore(str)
Convert CamelCase to camel_case.
slugify(str)
Converts a string to a slug suitable for a URL. Removes all whitespace and
symbols and replaces them with -
.
uniquify(tbl)
Iterates over array table tbl
appending all unique values into a new array
table, then returns the new one.
trim(str)
Trims the whitespace off of both sides of a string. Note that this function is
only aware of ASCII whitepsace characters, such as space, newline, tab, etc.
For full Unicode/UTF8 support see the lapis.util.utf8
module
trim_all(tbl)
Trims the whitespace off of all values in a table. Uses pairs
to traverse
every key in the table.
The table is modified in place.
trim_filter(tbl, [{keys ...}], [empty_val=nil])
Trims the whitespace off of all values in a table. The entry is removed from the table if the result is an empty string.
If an array table keys
is supplied then any other keys not in that list are
removed (with nil
, not the empty_val
)
If empty_val
is provided then the whitespace only values are replaced with
that value instead of nil
The table is modified in place.
local db = require("lapis.db")
local trim_filter = require("lapis.util").trim_filter
unknown_input = {
username = " hello ",
level = "admin",
description = " "
}
trim_filter(unknown_input, {"username", "description"}, db.NULL)
-- unknown input is now:
-- {
-- username = "hello",
-- description = db.NULL
-- }
db = require "lapis.db"
import trim_filter from require "lapis.util"
unknown_input = {
username: " hello "
level: "admin"
description: " "
}
trim_filter unknown_input, {"username", "description"}, db.NULL
-- unknown input is now:
-- {
-- username: "hello"
-- description: db.NULL
-- }
to_json(obj)
Converts obj
to JSON. Will strip recursion and things that can not be encoded.
from_json(str)
Converts JSON to table, a direct wrapper around Lua CJSON’s decode
.
time_ago_in_words(date, [parts=1], [suffix="ago"])
Returns a string in the format “1 day ago”.
parts
allows you to add more words. With parts=2
, the string
returned would be in the format 1 day, 4 hours ago
.
Encoding functions are found in:
local encoding = require("lapis.util.encoding")
encoding = require "lapis.util.encoding"
encode_base64(str)
Base64 encodes a string.
decode_base64(str)
Base64 decodes a string.
hmac_sha1(secret, str)
Calculates the hmac-sha1 digest of str
using secret
. Returns a binary
string.
encode_with_secret(object, secret=config.secret)
Encodes a Lua object and generates a signature for it. Returns a single string that contains the encoded object and signature.
decode_with_secret(msg_and_sig, secret=config.secret)
Decodes a string created by encode_with_secret
. The decoded object is only
returned if the signature is correct. Otherwise returns nil
and an error
message. The secret must match what was used with encode_with_secret
.
autoload(prefix, tbl={})
Makes it so accessing an unset value in tbl
will run a require
to search
for the value. Useful for autoloading components split across many files.
Overwrites __index
metamethod. The result of the require is stored in the
table.
local models = autoload("models")
local _ = models.HelloWorld --> will require "models.hello_world"
local _ = models.foo_bar --> will require "models.foo_bar"
models = autoload("models")
models.HelloWorld --> will require "models.hello_world"
models.foo_bar --> will require "models.foo_bar"
CSRF protection provides a way to prevent unauthorized requests that originate from other sites that are not your application. The common approach is to generate a special token that is placed on pages that make need to make calls with HTTP methods that are not safe (POST, PUT, DELETE, etc.). This token must be sent back to the server on the requests to verify the request came from a page generated by your application.
The default CSRF implementation generates a random string on the server and
stores it in the cookie. (The cookie’s name is your session name followed by
_token
.) The CSRF token is a cryptographically signed string that contains
the random string. You can optionally attach data to the CSRF token to control
how it can expire.
Before using any of the cryptographic functions it’s important to set your application’s secret. This is a string that only the application knows about. If your application is open source it’s worthwhile to not commit this secret. The secret is set in your configuration like so:
local config = require("lapis.config")
config("development", {
secret = "this is my secret string 123456"
})
config = require "lapis.config"
config "development", ->
secret "this is my secret string 123456"
Now that you have the secret configured, we might create a CSRF protected form like so:
local lapis = require("lapis")
local csrf = require("lapis.csrf")
local capture_errors = require("lapis.application").capture_errors
local app = lapis.Application()
app:get("form", "/form", function(self)
local csrf_token = csrf.generate_token(self)
self:html(function()
form({ method = "POST", action = self:url_for("form") }, function()
input({ type = "hidden", name = "csrf_token", value = csrf_token })
input({ type = "submit" })
end)
end)
end)
app:post("form", "/form", capture_errors(function(self)
csrf.assert_token(self)
return "The form is valid!"
end))
csrf = require "lapis.csrf"
class extends lapis.Application
[form: "/form"]: respond_to {
GET: =>
csrf_token = csrf.generate_token @
@html =>
form method: "POST", action: @url_for("form"), ->
input type: "hidden", name: "csrf_token", value: csrf_token
input type: "submit"
POST: capture_errors =>
csrf.assert_token @
"The form is valid!"
}
If you're using CSRF protection in a lot of actions then it might be helpful to create a before filter that generates the token automatically.
The following functions are part of the CSRF module:
local csrf = require("lapis.csrf")
csrf = require "lapis.csrf"
csrf.generate_token(req, data=nil)
Generates a token for the current session. If a random string has not been set
in the cookie yet, then it will be generated. You can optionally pass in data
to have it encoded into the token. You can then use the callback
parameter of
validate_token
to verify data’s value.
The random string is stored in a cookie named as your session name with
_token
appended to the end.
csrf.validate_token(req, callback=nil)
Validates the CSRF token located in req.params.csrf_token
. For any endpoints
you validation the token on you must pass the query or form parameter
csrf_token
with the value of the token returned by generate_token
.
If the validation fails then nil
and an error message are returned. A
callback function can be provided as the second argument. It’s a function that
will be called with the data payload stored in the token. You can specify the
data with the second argument of generate_token
.
Here’s an example of adding an expiration date using the token data:
local lapis = require("lapis")
local csrf = require("lapis.csrf")
local capture_errors = require("lapis.application").capture_errors
local app = lapis.Application()
app:get("form", "/form", function(self)
local csrf_token = csrf.generate_token(self, {
-- expire in 4 hours
expires = os.time() + 60*60*4
})
-- render a form using csrf_token...
end)
app:post("form", "/form", capture_errors(function(self)
csrf.assert_token(self, function(data)
if os.time() > (data.expires or 0) then
return nil, "token is expired"
end
return true
end)
return "The request is valid!"
end))
csrf = require "lapis.csrf"
class extends lapis.Application
[form: "/form"]: respond_to {
GET: =>
csrf_token = csrf.generate_token @, {
-- expire in 4 hours
expires: os.time! + 60*60*4
}
-- render a form using csrf_token...
POST: capture_errors =>
csrf.assert_token @, (d) ->
if os.time() > (d.expires or 0) then
return nil, "token is expired"
true
"The form is valid!"
}
csrf.assert_token(...)
First calls validate_token
with same arguments, then calls assert_error
if
validation fails.
Lapis comes with a built-in module for making asynchronous HTTP requests. The
way it works is by using the Nginx proxy_pass
directive on an internal
action. Because of this, before you can make any requests you need to modify
your Nginx configuration.
Add the following to your server block:
location /proxy {
internal;
rewrite_by_lua "
local req = ngx.req
for k,v in pairs(req.get_headers()) do
if k ~= 'content-length' then
req.clear_header(k)
end
end
if ngx.ctx.headers then
for k,v in pairs(ngx.ctx.headers) do
req.set_header(k, v)
end
end
";
resolver 8.8.8.8;
proxy_http_version 1.1;
proxy_pass $_url;
}
This code ensures that the correct headers are set for the new request. The
$_url
variable is used to store the target URL. It must be defined usingset $_url ""
directive in your default location.
Now we can use the lapis.nginx.http
module. There are two methods. request
and simple
. request
implements the Lua Socket HTTP request API (complete
with LTN12).
simple
is a simplified API with no LTN12:
local http = require("lapis.nginx.http")
local app = lapis.Application()
app:get("/", function(self)
-- a simple GET request
local body, status_code, headers = http.simple("http://leafo.net")
-- a post request, data table is form encoded and content-type is set to
-- application/x-www-form-urlencoded
http.simple("http://leafo.net/", {
name = "leafo"
})
-- manual invocation of the above request
http.simple({
url = "http://leafo.net",
method = "POST",
headers = {
["content-type"] = "application/x-www-form-urlencoded"
},
body = {
name = "leafo"
}
})
end)
http = require "lapis.nginx.http"
class extends lapis.Application
"/": =>
-- a simple GET request
body, status_code, headers = http.simple "http://leafo.net"
-- a post request, data table is form encoded and content-type is set to
-- application/x-www-form-urlencoded
http.simple "http://leafo.net/", {
name: "leafo"
}
-- manual invocation of the above request
http.simple {
url: "http://leafo.net"
method: "POST"
headers: {
"content-type": "application/x-www-form-urlencoded"
}
body: {
name: "leafo"
}
}
http.simple(req, body)
Performs an HTTP request using the internal /proxy
location.
Returns 3 values, the string result of the request, http status code, and a table of headers.
If there is only one argument and it is a string then that argument is treated as a URL for a GET request.
If there is a second argument it is set as the body of a POST request. If
the body is a table it is encoded with encode_query_string
and the
Content-type
header is set to application/x-www-form-urlencoded
If the first argument is a table then it is used to manually set request parameters. It takes the following keys:
url
— the URL to requestmethod
— "GET"
, "POST"
, "PUT"
, etc…body
— string or table which is encodedheaders
— a table of request headers to sethttp.request(url_or_table, body)
Implements a subset of Lua Socket’s
http.request
.
Does not support proxy
, create
, step
, or redirect
.
Lapis comes with a simple memory cache for caching the entire result of an action keyed on the parameters it receives. This is useful for speeding up the rendering of rarely changing pages because all database calls and HTML methods can be skipped.
The Lapis cache uses the shared dictionary API from HttpLuaModule. The first thing you'll need to do is create a shared dictionary in your Nginx configuration.
Add the following to your http
block to create a 15mb cache:
lua_shared_dict page_cache 15m;
Now we are ready to start using the caching module, lapis.cache
.
cache.cached(fn_or_tbl)
Wraps an action to use the cache.
local lapis = require("lapis")
local cached = require("lapis.cache").cached
local app = lapis.Application()
app:match("my_page", "/hello/world", cached(function(self)
return "hello world!"
end))
import cached from require "lapis.cache"
class extends lapis.Application
[my_page: "/hello/world"]: cached =>
"hello world!"
The first request to /hello/world
will run the action and store the result in
the cache, all subsequent requests will skip the action and return the text
stored in the cache.
The cache will remember not only the raw text output, but also the content type and status code.
The cache key also takes into account any GET parameters, so a request to
/hello/world?one=two
is stored in a separate cache slot. Multiple parameters
are sorted so they can come in any order and still match the same cache key.
When the cache is hit, a special response header is set to 1,
x-memory-cache-hit
. This is useful for debugging your application to make
sure the cache is working.
Instead of passing a function as the action of the cache you can also pass in a table. When passing in a table the function must be the first numerically indexed item in the table.
The table supports the following options:
dict_name
— override the name of the shared dictionary used (defaults to "page_cache"
)exptime
— how long in seconds the cache should stay alive, 0 is forever (defaults to 0
)cache_key
— set a custom function for generating the cache key (default is described above)when
— a function that should return truthy a value if the page should be cached. Receives the request object as first argument (defaults to nil
)For example, you could implement microcaching, where the page is cached for a short period of time, like so:
local lapis = require("lapis")
local cached = require("lapis.cache").cached
local app = lapis.Application()
app:match("/microcached", cached({
exptime = 1,
function(self)
return "hello world!"
end
}))
import cached from require "lapis.cache"
class extends lapis.Application
"/microcached": cached {
exptime: 1
=> "hello world!"
}
cache.delete(key, [dict_name="page_cache"])
Deletes an entry from the cache. Key can either be a plain string, or a tuple
of {path, params}
that will be encoded as the key.
local cache = require("lapis.cache")
cache.delete({ "/hello", { thing = "world" } })
cache = require "lapis.cache"
cache.delete { "/hello", { thing: "world" } }
cache.delete_all([dict_name="page_cache"])
Deletes all entries from the cache.
cache.delete_path(path, [dict_name="page_cache"])
Deletes all entries for a specific path.
local cache = require("lapis.cache")
cache.delete_path("/hello")
cache = require "lapis.cache"
cache.delete_path "/hello"
File uploads can be handled with a multipart form and accessing the file from
the @params
self.params
of the request.
For example, let’s create the following form:
import Widget from require "lapis.html"
class MyForm extends Widget
content: =>
form {
action: "/my_action"
method: "POST"
enctype: "multipart/form-data"
}, ->
input type: "file", name: "uploaded_file"
input type: "submit"
When the form is submitted, the file is stored as a table with filename
and
content
properties in @params
self.params
under the name of the form input:
local app = lapis.Application()
app:post("/my_action", function(self)
local file = self.params.uploaded_file
if file then
return "Uploaded: " .. file.filename .. ", " .. #file.content .. "bytes"
end
end)
class extends lapis.Application
"/my_action": =>
if file = @params.uploaded_file
"Uploaded #{file.filename}, #{#file.content}bytes"
A validation exists for ensuring that a param is an uploaded file, it’s called
is_file
:
local app = lapis.Application()
app:post("/my_action", function(self)
assert_valid(self.params, {
{ "uploaded_file", is_file = true }
})
-- file is ready to be used
end)
class extends lapis.Application
"/my_action": capture_errors =>
assert_valid @params, {
{ "uploaded_file", is_file: true }
}
-- file is ready to be used...
An uploaded file is loaded entirely into memory, so you should be careful about
the memory requirements of your application. Nginx limits the size of uploads
through the
client_max_body_size
directive. It’s only 1 megabyte by default, so if you plan to allow uploads
greater than that you should set a new value in your Nginx configuration.
The following functions are part of the lapis.application
module:
local app_helpers = require("lapis.application")
application = require "lapis.application"
respond_to(verbs_to_fn={})
verbs_to_fn
is a table of functions that maps a HTTP verb to a corresponding
function. Returns a new function that dispatches to the correct function in the
table based on the verb of the request. See
Handling HTTP verbs
If an action for HEAD
does not exist Lapis inserts the following function to
render nothing:
function() return { layout = false } end
-> { layout: false }
If the request is a verb that is not handled then the Lua error
function
is called and a 500 page is generated.
A special before
key can be set to a function that should run before any
other action. If @write
self.write
is called inside the before function then
the regular handler will not be called.
capture_errors(fn_or_tbl)
Wraps a function to catch errors sent by yield_error
or assert_error
. See
Exception Handling for more information.
If the first argument is a function then that function is called on request and the following default error handler is used:
function() return { render = true } end
-> { render: true }
If a table is the first argument then the 1
st element of the table is used as
the action and value of on_error
is used as the error handler.
When an error is yielded then the @errors
self.errors
variable is set on the current request and
the error handler is called.
capture_errors_json(fn)
A wrapper for capture_errors
that passes in the following error handler:
function(self) return { json = { errors = self.errors } } end
=> { json: { errors: @errors } }
yield_error(error_message)
Yields a single error message to be captured by capture_errors
obj, msg, ... = assert_error(obj, msg, ...)
Works like Lua’s assert
but instead of triggering a Lua error it triggers an
error to be captured by capture_errors
json_params(fn)
Return a new function that will parse the body of the request as JSON and
inject it into @params
self.params
if the content-type
is set to
application/json
. Suitable for wrapping an action handler to make it aware of
JSON encoded requests.
local json_params = require("lapis.application").json_params
app:match("/json", json_params(function(self)
return self.params.value
end))
import json_params from require "lapis.application"
class JsonApp extends lapis.Application
"/json": json_params =>
@params.value
$ curl \
-H "Content-type: application/json" \
-d '{"value": "hello"}' \
'https://localhost:8080/json'
The unmerged parameters can also be accessed from @json
self.json
. If there
was an error parsing the JSON then @json
self.json
will be nil
and the
request will continue without error.
This module includes a collection of LPeg patterns for working with UTF8 text.
local utf8 = requrie("lapis.util.utf8")
utf8 = requrie("lapis.util.utf8")
utf8.trim
A pattern that will trim all invisible characters from either side of the
matched string. (Utilizes the whitespace
pattern described below)
utf8.printable_character
A pattern that matches a single printable character. Note that printable characters include whitepace, but don’t include invalid unicode codepoints or control characters.
utf8.whitepace
An optimal pattern that matches any unicode codepoints that are classified as whitespace.