ACF mvc.lua 示例

来自 Alpine Linux

使用 mvc.lua 设置主机名

在此示例中,我们将使用 mvc.lua 创建一个简单的主机名设置命令行应用程序。构建控制器/模型后,您可以使用相同的代码通过 Web 和基于 Web 的应用程序控制器来设置主机名。


在此示例中,我们将假定您对您正在运行的 Linux 机器(最好是 Alpine Linux 机器!)具有 root 访问权限。

获取 mvc.lua 模块

从 git 仓库获取 mvc.lua 模块。

wget https://gitlab.alpinelinux.org/acf/acf-core/-/blob/master/lua/mvc.lua

创建模型和控制器

创建文件 hostname-controller.lua,定义“最终用户”可以运行的函数。我们将仅创建一个操作 edithostname,它将用于读取和更新主机名。由于该操作会更改系统,因此自然采用“表单”的形式

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

创建文件 hostname-model.lua,定义用于获取和设置主机名的模型函数。我们为每个函数返回一个 cfe 表,其中包括一个用于主机名的条目的表单

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

(可选)测试模型代码(不使用 mvc.lua)

如果需要,您可以创建一个 test.lua 脚本来验证模型代码是否可以独立工作

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)

然后您可以使用以下命令进行测试

#lua test.lua "Alpine"
 Alpine
#lua test.lua "Invalid Name"
 FAILED: Hostname can contain alphanumerics only
 Alpine

将软件包添加到 ACF 框架

为了使模型和控制器在 ACF mvc.lua 框架内工作,我们必须做几件事。

1. 安装 ACF 核心软件包

apk add acf-core

(可选)您可以添加整个基于 Web 的 ACF 框架

setup-acf

2. 修改 ACF 配置文件以在 /etc/acf/app/ 中查找其他软件包。编辑 /etc/acf/acf.conf 文件,将 /etc/acf/app/ 目录添加到 appdir 逗号分隔的列表中

appdir=/etc/acf/app/,/usr/share/acf/app/

3. 将模型和控制器移动到新的软件包目录。我们将软件包称为“test”

mkdir -p /etc/acf/app/test
mv hostname-*.lua /etc/acf/app/test

4. 使用 acf-cli 应用程序测试新软件包

# 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"

请注意,传递给 acf-cli 的操作字符串的格式为“/prefix/controller/action”。前缀是 /etc/acf/app 中可以找到控制器的路径,因此在我们的例子中为“/test/”。控制器由控制器文件名确定,因此在我们的例子中为“hostname”。操作对应于控制器中要调用的函数,因此在我们的例子中为“edithostname”。另请注意,在上面的两个示例中,第一种情况读取现有主机名,第二种情况更新主机名。acf-cli 应用程序的输出是 cfe 表单的序列化版本,这对于测试很有用,但在现实生活中不太有用。

在 ACF Web 界面中启用软件包

1. 添加基于 Web 的 ACF 框架

setup-acf

2. 配置所有用户都有权访问新的主机名操作。编辑 “/etc/acf/app/test/hostname.roles” 文件,并为 edithostname 操作添加 GUEST 权限

echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles

现在,通过浏览 https://IP-of-host/cgi-bin/acf/test/hostname/edithostname 应该可以看到新操作。显然,您可能需要重新考虑为该操作提供 GUEST 访问权限,因为这将允许未经身份验证的用户修改您的主机名。

3. 将新的主机名操作添加到 ACF 菜单。编辑 “/etc/acf/app/test/hostname.menu” 文件并添加一个菜单项

echo "Test Hostname Edit edithostname" > /etc/acf/app/test/hostname.menu

您需要先从 ACF 界面注销(或删除会话 cookie),然后新的菜单项才会可见。

创建基于 MVC 的应用程序

您有两种选择来创建您自己的基于 MVC 的应用程序。

1. 使用 dispatch 函数。这是 Web 界面和 acf-cli 应用程序使用的方法。要调度的操作以字符串格式 /prefix/controller/action 传递,输入值作为 clientinfo 表传递。输出由视图确定。

2. 使用 new 函数加载所需的控制器,然后直接调用控制器操作或模型函数。使用控制器操作需要使用 clientdata 结构来传递参数。对于调用模型函数,应用程序将调用 get 函数来检索表单 cfe,将所需的输入值填充到 cfe 中,然后调用 set 函数来提交表单并执行所需的操作。MVC 应用程序负责任何用户交互和结果显示。

使用 Dispatch 方法

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()

测试应用程序。如上面的代码所示,第一个参数是新的主机名,第二个参数是视图类型。

alpine:~# chmod 755 test_dispatch 
alpine:~# ./test_dispatch test
test:~# ./test_dispatch alpine
alpine:~#

请注意,应用程序可以运行,但没有输出。这是因为没有提供视图类型。内置支持以下标准视图类型(并非全部适用):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:~# 

您还可以使用自定义视图类型和/或视图来自定义输出。这依赖于 haserl 来解析视图文件,并且只有从 haserl 脚本启动时,haserl lua 函数才可用(如果 haserl 可以作为独立的 lua 库使用就好了)。Haserl 脚本希望由 Web 浏览器启动以处理 CGI 数据,因此传递输入比较棘手。我相信有更好的方法来做到这一点,但这只是一个例子

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:~# 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:~# 

您可以通过创建自己的应用程序特定控制器来进一步自定义应用程序。这就是 Web 界面和 acf-cli 应用程序的编写方式。在这两种情况下,都编写了一个简单的应用程序来加载的不仅是 mvc:new() 引用,而是一个应用程序特定的控制器来包装 mvc:new() 对象。然后对应用程序特定的控制器对象调用 dispatch 函数,该对象可以覆盖 mvc.lua 中的任何函数以添加自定义实现。例如,这两个应用程序都覆盖了 handle_clientdata 函数,以自定义在 clientdata 结构中提供输入数据的方式,并且 Web 界面特定的控制器包含大量代码来处理菜单、模板、皮肤、身份验证、重定向等功能... 理解这一点的最佳方法是直接阅读代码。

  • Web 应用程序
    • 应用程序 - acf
    • 控制器 - acf_www-controller.lua
  • acf-cli
    • 应用程序 - acf-cli
    • 控制器 - acf_cli-controller.lua

使用 New 方法

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()

一旦你有一个主机名控制器对象,你可以访问它的操作,通过 clientdata 传递数据,或者你可以直接访问模型的 get/set 函数。测试应用程序

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:~# 


此材料已过时...

请随时帮助我们制作最新版本。(讨论)

mvc 加载和执行特殊功能

mvc.lua 模块提供在模块加载时、执行控制器操作之前、执行控制器操作之后以及模块卸载时执行代码的功能。

这是通过控制器中的 mvc 表完成的。为了演示,让我们向 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

现在运行我们的脚本显示函数何时被调用

# 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

我们也可以向特定控制器添加 mvc 函数。将其添加到 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

结果如下

# 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

请注意,apphostnameon_loadon_unload 函数都已运行,但只有 hostnamepre_execpost_exec 函数运行了。这是因为 pre 和 post exec 函数作为“action”的一部分运行,并且 dispatch 函数在最低级别的控制器中查找 pre/post_exec 函数。由于 hostname 现在定义了这些函数,因此它会运行它们。

要同时运行 hostnameapp 的 pre_exec 函数,您必须安排 hostname 的 pre_exec 函数调用其父级的 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