ACF mvc.lua example: Difference between revisions
(Add example of MVC application using dispatch method) |
(update cgit to gitlab links) |
||
(4 intermediate revisions by 2 users not shown) | |||
Line 10: | Line 10: | ||
Get the mvc.lua module from the git repository. | Get the mvc.lua module from the git repository. | ||
wget | wget https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua | ||
== Create a model and controller == | == Create a model and controller == | ||
Line 166: | Line 166: | ||
echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles | echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles | ||
The new action should now be visible by browsing to ''https://IP-of-host/cgi-bin/acf/test/hostname/edithostname''. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname. | The new action should now be visible by browsing to ''<nowiki>https://IP-of-host/cgi-bin/acf/test/hostname/edithostname</nowiki>''. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname. | ||
3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item: | 3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item: | ||
Line 179: | Line 179: | ||
1. Use the ''dispatch'' function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format ''/prefix/controller/action'' and the input values are passed in as a clientinfo table. Output is determined by the view. | 1. Use the ''dispatch'' function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format ''/prefix/controller/action'' and the input values are passed in as a clientinfo table. Output is determined by the view. | ||
2. Use the ''new'' function to load the desired controller and then directly call the controller actions. For | 2. Use the ''new'' function to load the desired controller and then directly call the controller actions or model functions. Using the controller actions requires use of the clientdata structure to pass parameters. For calling the model functions, the application will call the get function to retrieve the form cfe, fill in the desired input values into the cfe, and then call the set function to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results. | ||
=== Use the Dispatch method === | === Use the Dispatch method === | ||
Line 271: | Line 271: | ||
alpine:~# | alpine:~# | ||
You can further customize the application by creating your own application-specific controller. This is how the web interface and the acf-cli application are written. In both cases, a simple application is written to load not just an mvc:new() reference, but an application-specific controller to wrap the mvc:new() object. The dispatch function is then called on the application-specific controller object, which can override any function in mvc.lua to add a customized implementation. For example, both applications override the handle_clientdata function to customize the way input data is provided in the clientdata structure, and the web interface-specific controller contains a lot of code to handle features such as menus, templates, skins, authentication, redirection, ... The best way to understand this is to just read the code. | |||
* Web application | |||
** Application - [https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua acf] | |||
** Controller - [https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua acf_www-controller.lua] | |||
* acf-cli | |||
** Application - [https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua acf-cli] | |||
** Controller - [https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua acf_cli-controller.lua] | |||
=== Use the New method === | |||
==== test_new ==== | |||
#!/usr/bin/lua | |||
-- Simple CLI based mvc application | -- Simple CLI based mvc application | ||
-- load the mvc module | |||
mvc = require("acf.mvc") | |||
-- load the module | |||
require("mvc") | |||
-- create an new "mvc object" | -- create an new "mvc object" | ||
Line 293: | Line 292: | ||
-- load the config file so we can find the appdir | -- load the config file so we can find the appdir | ||
MVC:read_config(" | MVC:read_config("acf") | ||
-- | -- load the hostname controller | ||
HOSTNAME=MVC:new("/test/hostname") | |||
local hostname | |||
-- METHOD 1 - controller action | |||
HOSTNAME.clientdata = {hostname=arg[1], submit="Update"} | |||
hostname = HOSTNAME:edithostname() | |||
-- METHOD 2 - model functions | |||
hostname = HOSTNAME.model:get_hostname() | |||
hostname.value.hostname.value = arg[1] | |||
hostname = HOSTNAME.model:set_hostname(hostname) | |||
if hostname.errtxt then | |||
print(hostname:print_errtxt()) | |||
else | |||
print(hostname.descr or "Hostname Updated") | |||
end | end | ||
HOSTNAME:destroy() | |||
MVC:destroy() | |||
Once you have a hostname controller object, you can access its actions, passing data through clientdata, or you can directly access the get/set functions of the model. Test the app: | |||
test:~# chmod 755 test_new | |||
test:~# ./test_new alpine | |||
Hostname Updated | |||
alpine:~# ./test_new asdfasdfasdfasdfasdf | |||
Failed to set hostname | |||
hostname: Hostname must be 16 characters or less | |||
alpine:~# | |||
{{Obsolete}} | |||
= mvc load & exec special functions = | = mvc load & exec special functions = |
Latest revision as of 20:56, 25 August 2023
Set the hostname with mvc.lua
In this example we will create a simple hostname-setting command-line application using mvc.lua. Once the controller/model are built, you can use the same code to set the hostname via the web with a web-based application controller.
For this example, we will assume you have root access on the linux box you are running on (preferably an Alpine Linux box!)
Get the mvc.lua module
Get the mvc.lua module from the git repository.
wget https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua
Create a model and controller
Create a file hostname-controller.lua, defining the functions that an "end user" could run. We will only create one action, edithostname, which will be used to read and update the hostname. Since the action makes changes to the system, it naturally takes the form of a 'form':
hostname-controller.lua
-- Controller for editing hostname local mymodule = {} mymodule.edithostname = function (self) return self.handle_form(self, self.model.get_hostname, self.model.set_hostname, self.clientdata, "Update", "Edit Hostname", "Hostname Updated") end return mymodule
Create a file hostname-model.lua, defining the model functions to get and set the hostname. We return a cfe table for each function including the form with the one entry for hostname:
hostname-model.lua
-- Model functions for retrieving / setting the hostname local mymodule = {} -- Create a cfe defining the form for editing the hostname and containing the current value mymodule.get_hostname = function(self, clientdata) local retval = cfe({ type="group", value={}, label="Hostname" }) -- Warning - io.popen has security risks, never pass user data to io.popen local f = io.popen ("/bin/hostname") local n = f:read("*a") or "none" f:close() n=string.gsub(n, "\n$", "") retval.value.hostname = cfe({ value=n, label="Hostname" }) return retval end -- Set the hostname from the value contained in the cfe created by get_hostname mymodule.set_hostname = function(self, hostnameform, action) local success = true -- Check to make sure the name is valid if (hostnameform.value.hostname.value == "") then success = false hostnameform.value.hostname.errtxt = "Hostname must not be blank" elseif (#hostnameform.value.hostname.value > 16) then success = false hostnameform.value.hostname.errtxt = "Hostname must be 16 characters or less" elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then success = false hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only" end -- If it is valid, set the hostname if ( success ) then local f = io.open("/etc/hostname", "w") if f then f:write(hostnameform.value.hostname.value .. "\n") f:close() end -- Warning - io.popen has security risks, never pass user data to io.popen f = io.popen ("/bin/hostname -F /etc/hostname") f:close() else hostnameform.errtxt = "Failed to set hostname" end return hostnameform end return mymodule
Optionally test the model code (without mvc.lua)
If you want, you can create a test.lua script to validate the model code works on its own:
test.lua
require("mvc") -- Needed for cfe function definition m=require("hostname-model") local form = m.get_hostname() form.value.hostname.value = arg[1] or "" form = m.set_hostname(nil, form) if form.errtxt then print("FAILED: "..form.value.hostname.errtxt or form.errtxt) end form = m.get_hostname() print(form.value.hostname.value)
You can then test this with:
#lua test.lua "Alpine" Alpine
#lua test.lua "Invalid Name" FAILED: Hostname can contain alphanumerics only Alpine
Add the package to the ACF framework
To make the model and controller work within the ACF mvc.lua framework, we must do several things.
1. Install the ACF core package:
apk add acf-core
Optionally, you can add the entire web-based ACF framework:
setup-acf
2. Modify the ACF configuration file to look in /etc/acf/app/ for additional packages. Edit the /etc/acf/acf.conf file to add the /etc/acf/app/ directory to the appdir comma-separated list:
appdir=/etc/acf/app/,/usr/share/acf/app/
3. Move the model and controller to the new package directory. We will call the package "test":
mkdir -p /etc/acf/app/test mv hostname-*.lua /etc/acf/app/test
4. Test the new package using the acf-cli application:
# acf-cli /test/hostname/edithostname result = {} result["label"] = "Edit Hostname" result["option"] = "Edit" result["type"] = "form" result["value"] = {} result["value"]["hostname"] = {} result["value"]["hostname"]["label"] = "Hostname" result["value"]["hostname"]["type"] = "text" result["value"]["hostname"]["value"] = "Alpine" # acf-cli /test/hostname/edithostname hostname=test submit=true result = {} result["descr"] = "Hostname Updated" result["label"] = "Edit Hostname" result["option"] = "Edit" result["type"] = "form" result["value"] = {} result["value"]["hostname"] = {} result["value"]["hostname"]["label"] = "Hostname" result["value"]["hostname"]["type"] = "text" result["value"]["hostname"]["value"] = "test"
Note that the action string passed to acf-cli is of the form "/prefix/controller/action". The prefix is the path within /etc/acf/app where the controller may be found, so "/test/" for our case. The controller is determined from the controller file name, so "hostname" for our case. And, the action corresponds to the function within the controller to call, so "edithostname" for our case. Also note that, in the two examples above, the first case reads the existing hostname and the second case updates it. The output of the acf-cli application is a serialized version of the cfe form, which is good for testing but not too useful in real life.
Enable the package in the ACF web interface
1. Add the web-based ACF framework:
setup-acf
2. Configure all users to have access to the new hostname action. Edit "/etc/acf/app/test/hostname.roles" file and add GUEST permission for the edithostname action:
echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles
The new action should now be visible by browsing to https://IP-of-host/cgi-bin/acf/test/hostname/edithostname. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname.
3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item:
echo "Test Hostname Edit edithostname" > /etc/acf/app/test/hostname.menu
You will need to log off from the ACF interface (or delete the session cookie) before the new menu item will be visible.
Make an MVC based application
You have two options for creating an MVC based application of your own.
1. Use the dispatch function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format /prefix/controller/action and the input values are passed in as a clientinfo table. Output is determined by the view.
2. Use the new function to load the desired controller and then directly call the controller actions or model functions. Using the controller actions requires use of the clientdata structure to pass parameters. For calling the model functions, the application will call the get function to retrieve the form cfe, fill in the desired input values into the cfe, and then call the set function to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results.
Use the Dispatch method
test_dispatch
#!/usr/bin/lua -- Simple CLI based mvc application -- load the mvc module mvc = require("acf.mvc") -- create an new "mvc object" MVC=mvc:new() -- load the config file so we can find the appdir MVC:read_config("acf") -- dispatch the request local clientdata = {hostname=arg[1], viewtype=arg[2], submit="Update"} MVC:dispatch("/test/", "hostname", "edithostname", clientdata) -- destroy the mvc object MVC:destroy()
Test the application. As can be seen in the code above, the first argument is the new hostname and the second is a viewtype.
alpine:~# chmod 755 test_dispatch alpine:~# ./test_dispatch test test:~# ./test_dispatch alpine alpine:~#
Note that the application functions, but there is no output. This is because there is no viewtype supplied. There is built-in support for these standard viewtypes (not all apply): html, json, stream, serialized
alpine:~# ./test_dispatch test serialized result = {} result["descr"] = "Hostname Updated" result["label"] = "Edit Hostname" result["option"] = "Update" result["type"] = "form" result["value"] = {} result["value"]["hostname"] = {} result["value"]["hostname"]["label"] = "Hostname" result["value"]["hostname"]["type"] = "text" result["value"]["hostname"]["value"] = "test" test:~# ./test_dispatch alpine json {"type":"form","label":"Edit Hostname","value":{"hostname":{"value":"alpine","type":"text","label":"Hostname"}},"option":"Update","descr":"Hostname Updated"} alpine:~#
You can also use a custom viewtype and/or view to customize the output. This relies on haserl to parse the view file, and haserl lua functions are only available when launched from haserl scripts (it would be nice if haserl were available as a standalone lua library). Haserl scripts expect to be launched by a web browser to handle CGI data, so passing input is trickier. I'm sure there is a better way to do this, but this is just an example:
test_haserl
#!/usr/bin/haserl-lua5.2 --shell=lua <% -- Simple CLI based mvc application -- load the mvc module mvc = require("acf.mvc") -- create an new "mvc object" MVC=mvc:new() -- load the config file so we can find the appdir MVC:read_config("acf") -- dispatch the request local clientdata = {hostname=FORM.hostname, viewtype=FORM.viewtype, submit="Update"} MVC:dispatch("/test/", "hostname", "edithostname", clientdata) -- destroy the mvc object MVC:destroy() %>
/etc/acf/app/test/hostname-edithostname-text.lsp
<% local data, viewlibrary, page_info, session = ... if data.errtxt then print(data:print_errtxt()) else print(data.descr) end %>
Test the application
test:~# chmod 755 test_haserl test:~# QUERY_STRING='hostname=alpine&viewtype=text' REQUEST_METHOD=GET ./test_haserl Hostname Updated alpine:~# QUERY_STRING='hostname=asdfasdfasdfasdfasdf&viewtype=text' REQUEST_METHOD=GET ./test_haserl Failed to set hostname hostname: Hostname must be 16 characters or less alpine:~#
You can further customize the application by creating your own application-specific controller. This is how the web interface and the acf-cli application are written. In both cases, a simple application is written to load not just an mvc:new() reference, but an application-specific controller to wrap the mvc:new() object. The dispatch function is then called on the application-specific controller object, which can override any function in mvc.lua to add a customized implementation. For example, both applications override the handle_clientdata function to customize the way input data is provided in the clientdata structure, and the web interface-specific controller contains a lot of code to handle features such as menus, templates, skins, authentication, redirection, ... The best way to understand this is to just read the code.
- Web application
- Application - acf
- Controller - acf_www-controller.lua
- acf-cli
- Application - acf-cli
- Controller - acf_cli-controller.lua
Use the New method
test_new
#!/usr/bin/lua -- Simple CLI based mvc application -- load the mvc module mvc = require("acf.mvc") -- create an new "mvc object" MVC=mvc:new() -- load the config file so we can find the appdir MVC:read_config("acf") -- load the hostname controller HOSTNAME=MVC:new("/test/hostname") local hostname -- METHOD 1 - controller action HOSTNAME.clientdata = {hostname=arg[1], submit="Update"} hostname = HOSTNAME:edithostname() -- METHOD 2 - model functions hostname = HOSTNAME.model:get_hostname() hostname.value.hostname.value = arg[1] hostname = HOSTNAME.model:set_hostname(hostname) if hostname.errtxt then print(hostname:print_errtxt()) else print(hostname.descr or "Hostname Updated") end HOSTNAME:destroy() MVC:destroy()
Once you have a hostname controller object, you can access its actions, passing data through clientdata, or you can directly access the get/set functions of the model. Test the app:
test:~# chmod 755 test_new test:~# ./test_new alpine Hostname Updated alpine:~# ./test_new asdfasdfasdfasdfasdf Failed to set hostname hostname: Hostname must be 16 characters or less alpine:~#
This material is obsolete ... Please feel free to help us make an up-to-date version. (Discuss) |
mvc load & exec special functions
The mvc.lua module has a provision for executing code on module load, prior to executing the controller's action, just after executing the controller's action, and on module unload.
This is done with the mvc table in the controller. To demonstrate, let's add a few functions to helloworld/app/app-controller.lua
mvc = {} mvc.on_load = function (self, parent) print ("This is the app controller's on_load function") end mvc.pre_exec = function (self) print ("This is the app controller's pre_exec function") end mvc.post_exec = function (self) print ("This is the app controller's post_exec function") end
mvc.on_unload = function (self) print ("This is the app controller's on_unload function") end
Now running our script shows when the functions get called:
# lua helloworld.lua update "Alpine" This is the app controller's on_load function This is the app controller's pre_exec function This is the app controller's post_exec function Controller: hostname Action: update Returned a table with the following values: value Alpine type string This is the app controller's on_unload function
We can add mvc functions to a specific controller, as well. Add this to helloworld/app/hostname-controller.lua
mvc = {} mvc.on_load = function (self, parent) print ("This is the hostname controller's on_load function") end mvc.pre_exec = function (self) print ("This is the hostname controller's pre_exec function") end mvc.post_exec = function (self) print ("This is the hostname controller's post_exec function") end mvc.on_unload = function (self) print ("This is the hostname controller's on_unload function") end
And this happens:
# lua helloworld.lua update "Alpine" This is the app controller's on_load function This is the hostname controller's on_load function This is the hostname controller's pre_exec function This is the hostname controller's post_exec function This is the hostname controller's on_unload function Controller: hostname Action: update Returned a table with the following values: value Alpine type string This is the app controller's on_unload function
Note that both the app and hostname on_load and on_unload functions were run, but only the hostname pre_exec and post_exec functions ran. This is because the pre and post exec functions are run as part of the "action", and the dispatch function looks in the lowest-level controller for the pre/post_exec function. Since hostname now defines those functions, it runs them.
To run both the hostname and app pre_exec function, you must arrange for the hostname pre_exec function to call it's parent pre_exec:
mvc = {} mvc.on_load = function (self, parent) print ("This is the hostname controller's on_load function") mvc.parent_pre_exec = parent.worker.mvc.pre_exec end mvc.pre_exec = function (self) mvc.parent_pre_exec (self) print ("This is the hostname controller's pre_exec function") end