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:
localutil=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 andsymbols and replaces them with-.
uniquify(tbl)Iterates over array tabletbl appending all unique values into a new arraytable, then returns the new one.
trim(str)Trims the whitespace off of both sides of a string. Note that this function isonly aware of ASCII whitepsace characters, such as space, newline, tab, etc.For full Unicode/UTF8 support see thelapis.util.utf8 module
trim_all(tbl)Trims the whitespace off of all values in a table. Usespairs to traverseevery 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 fromthe table if the result is an empty string.
If an array tablekeys is supplied then any other keys not in that list areremoved (withnil, not theempty_val)
Ifempty_val is provided then the whitespace only values are replaced withthat value instead ofnil
The table is modified in place.
localdb=require("lapis.db")localtrim_filter=require("lapis.util").trim_filterunknown_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"importtrim_filterfromrequire"lapis.util"unknown_input={username:" hello "level:"admin"description:" "}trim_filterunknown_input,{"username","description"},db.NULL-- unknown input is now:-- {-- username: "hello"-- description: db.NULL-- }to_json(obj)Convertsobj 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’sdecode.
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. Withparts=2, the stringreturned would be in the format1 day, 4 hours ago.
Encoding functions are found in:
localencoding=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 ofstr usingsecret. Returns a binarystring.
encode_with_secret(object, secret=config.secret)Encodes a Lua object and generates a signature for it. Returns a single stringthat contains the encoded object and signature.
decode_with_secret(msg_and_sig, secret=config.secret)Decodes a string created byencode_with_secret. The decoded object is onlyreturned if the signature is correct. Otherwise returnsnil and an errormessage. The secret must match what was used withencode_with_secret.
autoload(prefix, tbl={})Makes it so accessing an unset value intbl will run arequire to searchfor the value. Useful for autoloading components split across many files.Overwrites__index metamethod. The result of the require is stored in thetable.
localmodels=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 originatefrom other sites that are not your application. The common approach is togenerate a special token that is placed on pages that make need to make callswith HTTP methods that are notsafe (POST, PUT, DELETE, etc.). This tokenmust be sent back to the server on the requests to verify the request came froma page generated by your application.
The default CSRF implementation generates a random string on the server andstores it in the cookie. (The cookie’s name is your session name followed by_token.) The CSRF token is a cryptographically signed string that containsthe random string. You can optionally attach data to the CSRF token to controlhow it can expire.
Before using any of the cryptographic functions it’s important to set yourapplication’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 inyour configuration like so:
localconfig=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 formlike so:
locallapis=require("lapis")localcsrf=require("lapis.csrf")localcapture_errors=require("lapis.application").capture_errorslocalapp=lapis.Application()app:get("form","/form",function(self)localcsrf_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"classextendslapis.Application[form:"/form"]:respond_to{GET:=>csrf_token=csrf.generate_token@@html=>formmethod:"POST",action:@url_for("form"),->inputtype:"hidden",name:"csrf_token",value:csrf_tokeninputtype:"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 helpfulto create a before filter that generates the token automatically.
The following functions are part of the CSRF module:
localcsrf=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 setin the cookie yet, then it will be generated. You can optionally pass in datato have it encoded into the token. You can then use thecallback parameter ofvalidate_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 inreq.params.csrf_token. For any endpointsyou validation the token on you must pass the query or form parametercsrf_token with the value of the token returned bygenerate_token.
If the validation fails thennil and an error message are returned. Acallback function can be provided as the second argument. It’s a function thatwill be called with the data payload stored in the token. You can specify thedata with the second argument ofgenerate_token.
Here’s an example of adding an expiration date using the token data:
locallapis=require("lapis")localcsrf=require("lapis.csrf")localcapture_errors=require("lapis.application").capture_errorslocalapp=lapis.Application()app:get("form","/form",function(self)localcsrf_token=csrf.generate_token(self,{-- expire in 4 hoursexpires=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)ifos.time()>(data.expiresor0)thenreturnnil,"token is expired"endreturntrueend)return"The request is valid!"end))csrf=require"lapis.csrf"classextendslapis.Application[form:"/form"]:respond_to{GET:=>csrf_token=csrf.generate_token@,{-- expire in 4 hoursexpires:os.time!+60*60*4}-- render a form using csrf_token...POST:capture_errors=>csrf.assert_token@,(d)->ifos.time()>(d.expiresor0)thenreturnnil,"token is expired"true"The form is valid!"}csrf.assert_token(...)First callsvalidate_token with same arguments, then callsassert_error ifvalidation fails.
Lapis comes with a built-in module for making asynchronous HTTP requests. Theway it works is by using the Nginxproxy_pass directive on an internalaction. Because of this, before you can make any requests you need to modifyyour 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 "; resolver8.8.8.8; proxy_http_version1.1;proxy_pass$_url;}This code ensures that the correct headers are set for the new request. The
$_urlvariable is used to store the target URL. It must be defined usingset $_url ""directive in your default location.
Now we can use thelapis.nginx.http module. There are two methods.requestandsimple.request implements the Lua Socket HTTP request API (completewith LTN12).
simple is a simplified API with no LTN12:
localhttp=require("lapis.nginx.http")localapp=lapis.Application()app:get("/",function(self)-- a simple GET requestlocalbody,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-urlencodedhttp.simple("http://leafo.net/",{name="leafo"})-- manual invocation of the above requesthttp.simple({url="http://leafo.net",method="POST",headers={["content-type"]="application/x-www-form-urlencoded"},body={name="leafo"}})end)http=require"lapis.nginx.http"classextendslapis.Application"/":=>-- a simple GET requestbody,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-urlencodedhttp.simple"http://leafo.net/",{name:"leafo"}-- manual invocation of the above requesthttp.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 atable of headers.
If there is only one argument and it is a string then that argument is treatedas a URL for a GET request.
If there is a second argument it is set as the body of a POST request. Ifthe body is a table it is encoded withencode_query_string and theContent-type header is set toapplication/x-www-form-urlencoded
If the first argument is a table then it is used to manually set requestparameters. 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 ofLua Socket’shttp.request.
Does not supportproxy,create,step, orredirect.
Lapis comes with a simple memory cache for caching the entire result of anaction keyed on the parameters it receives. This is useful for speeding up therendering of rarely changing pages because all database calls and HTML methodscan be skipped.
The Lapis cache uses theshared dictionaryAPI from HttpLuaModule.The first thing you'll need to do is create a shared dictionary in your Nginxconfiguration.
Add the following to yourhttp block to create a 15mb cache:
lua_shared_dict page_cache15m;Now we are ready to start using the caching module,lapis.cache.
cache.cached(fn_or_tbl)Wraps an action to use the cache.
locallapis=require("lapis")localcached=require("lapis.cache").cachedlocalapp=lapis.Application()app:match("my_page","/hello/world",cached(function(self)return"hello world!"end))importcachedfromrequire"lapis.cache"classextendslapis.Application[my_page:"/hello/world"]:cached=>"hello world!"The first request to/hello/world will run the action and store the result inthe cache, all subsequent requests will skip the action and return the textstored in the cache.
The cache will remember not only the raw text output, but also the contenttype 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 parametersare 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 makesure the cache is working.
Instead of passing a function as the action of the cache you can also pass in atable. When passing in a table the function must be the first numericallyindexed 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 to0)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 tonil)For example, you could implement microcaching, where the page is cached for ashort period of time, like so:
locallapis=require("lapis")localcached=require("lapis.cache").cachedlocalapp=lapis.Application()app:match("/microcached",cached({exptime=1,function(self)return"hello world!"end}))importcachedfromrequire"lapis.cache"classextendslapis.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 tupleof{path, params} that will be encoded as the key.
localcache=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.
localcache=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 fromthe@paramsself.params of the request.
For example, let’s create the following form:
importWidgetfromrequire"lapis.html"classMyFormextendsWidgetcontent:=>form{action:"/my_action"method:"POST"enctype:"multipart/form-data"},->inputtype:"file",name:"uploaded_file"inputtype:"submit"When the form is submitted, the file is stored as a table withfilename andcontent properties in@paramsself.params under the name of the form input:
localapp=lapis.Application()app:post("/my_action",function(self)localfile=self.params.uploaded_fileiffilethenreturn"Uploaded: "..file.filename..", "..#file.content.."bytes"endend)classextendslapis.Application"/my_action":=>iffile=@params.uploaded_file"Uploaded #{file.filename}, #{#file.content}bytes"A validation exists for ensuring that a param is an uploaded file, it’s calledis_file:
localapp=lapis.Application()app:post("/my_action",function(self)assert_valid(self.params,{{"uploaded_file",is_file=true}})-- file is ready to be usedend)classextendslapis.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 aboutthe memory requirements of your application. Nginx limits the size of uploadsthrough theclient_max_body_sizedirective. It’s only 1 megabyte by default, so if you plan to allow uploadsgreater than that you should set a new value in your Nginx configuration.
The following functions are part of thelapis.application module:
localapp_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 correspondingfunction. Returns a new function that dispatches to the correct function in thetable based on the verb of the request. SeeHandling HTTP verbs
If an action forHEAD does not exist Lapis inserts the following function torender nothing:
function()return{layout=false}end->{layout:false}If the request is a verb that is not handled then the Luaerror functionis called and a 500 page is generated.
A specialbefore key can be set to a function that should run before anyother action. If@writeself.write is called inside the before function thenthe regular handler will not be called.
capture_errors(fn_or_tbl)Wraps a function to catch errors sent byyield_error orassert_error. SeeException Handling for more information.
If the first argument is a function then that function is called on request andthe following default error handler is used:
function()return{render=true}end->{render:true}If a table is the first argument then the1st element of the table is used asthe action and value ofon_error is used as the error handler.
When an error is yielded then the@errorsself.errors variable is set on the current request andthe error handler is called.
capture_errors_json(fn)A wrapper forcapture_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 bycapture_errors
obj, msg, ... = assert_error(obj, msg, ...)Works like Lua’sassert but instead of triggering a Lua error it triggers anerror to be captured bycapture_errors
json_params(fn)Return a new function that will parse the body of the request as JSON andinject it into@paramsself.params if thecontent-type is set toapplication/json. Suitable for wrapping an action handler to make it aware ofJSON encoded requests.
localjson_params=require("lapis.application").json_paramsapp:match("/json",json_params(function(self)returnself.params.valueend))importjson_paramsfromrequire"lapis.application"classJsonAppextendslapis.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@jsonself.json. If therewas an error parsing the JSON then@jsonself.json will benil and therequest will continue without error.
This module includes a collection of LPeg patterns for working with UTF8 text.
localutf8=requrie("lapis.util.utf8")utf8=requrie("lapis.util.utf8")utf8.trimA pattern that will trim all invisible characters from either side of thematched string. (Utilizes thewhitespace pattern described below)
utf8.printable_characterA pattern that matches a single printable character. Note that printablecharacters include whitepace, but don’t include invalid unicode codepoints orcontrol characters.
utf8.whitepaceAn optimal pattern that matches any unicode codepoints that are classified aswhitespace.