├── web ├── robots.txt ├── favicon.ico ├── css │ ├── site.css │ ├── bootstrap-theme.min.css │ └── bootstrap-theme.css └── js │ └── bootstrap.min.js ├── .gitignore ├── runtime ├── win-start.bat ├── win-stop.bat ├── stop.sh ├── start.sh ├── nginx.conf └── mime.types ├── .gitattributes ├── views ├── site │ ├── error.lua.html │ ├── index.lua.html │ ├── login.lua.html │ └── guide.lua.html └── layout │ └── main.lua.html ├── config ├── web.lua ├── memcache.lua ├── db.lua ├── lang.lua └── session.lua ├── index.lua ├── controllers └── site.lua ├── models ├── Userinfo.lua └── LoginForm.lua ├── vendor ├── ActiveRecord.lua ├── Memcache.lua ├── Request.lua ├── Application.lua ├── User.lua ├── resty │ ├── template │ │ ├── html.lua │ │ └── microbenchmark.lua │ └── template.lua ├── Mysql.lua ├── Files.lua ├── Session.lua ├── Pager.lua ├── Model.lua ├── Controller.lua ├── Util.lua └── Query.lua ├── LICENSE.txt ├── lua-releng ├── README-cn.md └── README.md /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.log 3 | *.pid 4 | *_temp -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylun/lua-resty-yii/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /runtime/win-start.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylun/lua-resty-yii/HEAD/runtime/win-start.bat -------------------------------------------------------------------------------- /runtime/win-stop.bat: -------------------------------------------------------------------------------- 1 | title ֹͣlua-resty-yii 2 | 3 | taskkill /F /IM nginx.exe /T 4 | 5 | pause -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=Lua 2 | *.css linguist-language=Lua 3 | *.html linguist-language=Lua 4 | *.lua linguist-language=Lua 5 | -------------------------------------------------------------------------------- /runtime/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0`/../ 3 | 4 | ps -ef|grep `pwd`|grep openresty |grep -v grep |awk '{print $2}'|xargs kill 5 | -------------------------------------------------------------------------------- /views/site/error.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | {% 3 | context.title = lang.infoNote 4 | %} 5 |
您已经成功创建了一个Lua-Resty-Yii应用。
10 |Lua-Resty-Yii 是在OpenResty的基础上开发的Web框架,具有强大的运行性能。
18 |Lua-Resty-Yii 是参考PHP的著名开源框架Yii2搭建,使用十分简单易用,易于开发扩展。
22 |Lua-Resty-Yii 可帮助您开发清洁和可重用的代码。它遵循了 MVC 模式,确保了清晰分离逻辑层和表示层。
26 |目录及文件:
9 |10 | |____index.lua #入口文件 11 | |____lua-releng #检查代码质量/是否使用了全局变量 12 | |____config #配置文件目录 13 | | |____db.lua #数据库配置 14 | | |____lang.lua #语言包配置 15 | | |____memcache.lua #memcache配置 16 | | |____session.lua #session配置 17 | | |____web.lua #网站基本信息配置 18 | |____controllers #controller目录 19 | | |____site.lua #前端页面 20 | |____models #models目录 21 | | |____LoginForm.lua #登录逻辑处理类 22 | | |____Userinfo.lua #用户信息管理类 23 | |____runtime #Openresty运行目录 24 | | |____client_body_temp #post上传的临时保存目录 25 | | |____fastcgi_temp #fastcig临时目录 26 | | |____logs #站点日志目录 27 | | | |____access.log #请求日志 28 | | | |____error.log #错误日志 29 | | | |____nginx.pid #nginx进程文件 30 | | |____nginx.conf #站点nginx配置文件 31 | | |____proxy_temp #proxy_temp 32 | | |____scgi_temp #scgi_temp 33 | | |____start.sh #站点启动程序 34 | | |____stop.sh #站点停止程序 35 | | |____uwsgi_temp #uwsgi_temp 36 | |____vendor #框架及第三方类 37 | | |____ActiveRecord.lua #数据库处理基类 38 | | |____Application.lua #请求处理类 39 | | |____Controller.lua #Controller基类 40 | | |____Files.lua #上传文件接收类 41 | | |____Memcache.lua #Memcache操作类 42 | | |____Model.lua #Model基类 43 | | |____Mysql.lua #Mysql操作类 44 | | |____Pager.lua #分页类 45 | | |____Query.lua #查询构建器 46 | | |____Request.lua #请求信息工具类 47 | | |____resty #第三方resty工具 48 | | | |____http.lua #http请求工具 49 | | | |____template.lua #lua模版工具类 50 | | |____Session.lua #Session操作类 51 | | |____User.lua #用户信息操作类 52 | | |____Util.lua #基本工具类 53 | |____views #页面模版目录 54 | | |____layout #页面框架目录 55 | | | |____main.lua.html #基本站点框架 56 | | |____site #站点页面目录 57 | | | |____error.lua.html #错误信息页 58 | | | |____guide.lua.html #开发说明页 59 | | | |____index.lua.html #首页 60 | | | |____login.lua.html #登录页 61 | |____web #静态资源目录 62 | | |____css #样式 63 | | | |____bootstrap-theme.css #bootstrap 64 | | | |____bootstrap-theme.min.css #bootstrap 65 | | | |____bootstrap.css #bootstrap 66 | | | |____bootstrap.min.css #bootstrap 67 | | | |____site.css #站点样式 68 | | |____js #javascript 69 | | | |____bootstrap.js #bootstrap 70 | | | |____bootstrap.min.js #bootstrap 71 | | | |____jquery.js #jquery 72 | | | |____jquery.min.js #jquery 73 | | | |____jquery.slim.js #jquery 74 | | | |____jquery.slim.min.js #jquery 75 | | |____favicon.ico #图标 76 | | |____robots.txt #robots.txt 77 |78 |
运行机制概述
79 |80 | 每一次应用开始处理 HTTP 请求时,它都会进行一个近似的流程。 81 | 82 | 用户提交指向 入口脚本 index.lua 的请求 83 | 入口脚本会创建一个 应用(Application) 实例用于处理该请求,并加载 配置。 84 | 应用会通过 request(请求) 应用组件解析被请求的 路由。 85 | 应用创建一个 controller(控制器) 实例具体处理请求。 86 | 执行controller中的before()方法进行请求过滤 87 | 如果执行before()返回true,则继续执行 action(动作),否则终止 88 | 动作会加载一个数据模型,一般是从数据库中加载。 89 | 动作会渲染一个 View(视图),并为其提供所需的数据模型。 90 | 渲染得到的结果会返回给 response(响应) 应用组件。 91 | 响应组件会把渲染结果发回给用户的浏览器。 92 |93 | 94 |
调试模式:
95 |96 | 访问 /lua/index 会使用lua_code_cache off的模式访问站点 97 | 访问 /lua/<filepath> 可调试对应的lua脚本 98 |99 | 100 |
应用(Application)
101 |
102 | 每接收到一个 HTTP 请求,入口脚本 index.lua 会创建一个 应用(Application) 实例用于处理该请求
103 | Application实例创建后,会保存覆盖全局变量ngx.ctx(此全局变量生命周期对应每个请求)
104 |
105 | ngx.ctx = require("vendor.Application"):new()
106 |
107 | Application在创建时,会加载网站的基本配置及基本工具作为属性,如:
108 |
109 | ngx.ctx.web 相当于 require("config.web"), --站点配置信息表
110 | ngx.ctx.lang 相当于 require("config.lang"), --语言包配置信息
111 | ngx.ctx.session 相当于 require("config.session"), --站点Session类
112 | ngx.ctx.user 相当于 require("vendor.User"), --站点用户工具类
113 | ngx.ctx.request 相当于 require("vendor.Request"), --站点请求处理类
114 |
115 | 在执行Application的new()时,会创建一个继承于Application的Controller实例,
116 | 因此ngx.ctx即是一个Application也是一个Controller实例,并且controller中也可以通过
117 | self.web,self.lang,self.session.self.user.self.request直接引用以上配置及工具
118 |
119 | 注意:在使用时,尽量少使用全局变量ngx.ctx,而应该通过函数传值的方式进行传递,能获得更快的运行速度
120 |
121 | 控制器(Controllers):
122 |
123 | local _M = require("vendor.Controller"):new{_VERSION='0.01'} --生成Controller新实例
124 |
125 | function _M:indexAction() --动作方法名必须以name+Action组成
126 | if not self.user.isLogin then return self:loginAction() end --使用用户信息类判断用户是否登录
127 |
128 | local Product = require('models.Product') --使用Product数据表操作类
129 | local query = Product.find().where{'in','product_id',self.user.resource} --fin()方法生成新的查询器
130 |
131 | local page = Pager{ --使用分页类
132 | pageSize = 8, --设置每页显示最多8条信息
133 | totalCount = query.count(), --使用查询器查询符合条件的数据总条数
134 | }
135 |
136 | local list = query.orderBy('create_time desc').page(page).all() --使用查询器查询分页数据集
137 |
138 | return self:render('index',{
139 | page = page,
140 | list = list,
141 | })
142 | end
143 |
144 | retrun _M
145 |
146 | 如果保存为controllers/filename.lua,则访问?act=filename.index时,会执行上面对应的indexAction()方法
147 |
148 | 模型(Models)
149 |
150 | 模型是 MVC 模式中的一部分, 是代表业务数据、规则和逻辑的对象。
151 | 可通过继承 "vendor.Model" 或它的子类定义模型类, 基类"vendor.Model"支持许多实用的特性,如:
152 |
153 | local _M = require("vendor.Model"):new{_version='0.0.1'} --生成Model新实例
154 |
155 | local Userinfo = require('models.Userinfo') --在Model里使用其他model
156 |
157 | function _M.rules() --方法rules添加数据校验规则
158 | return {
159 | {{'username','password','sacode'}, 'trim'}, --通过trim自动过滤输入
160 | {'username', 'required',message='请填写登录账号'}, --通过required规则设置必须填写
161 | --{'username', 'email'}, --如果需要用户名必须为email时设置
162 | {'password', 'required',message='请填写登录密码'},
163 | --使用自定义方法校验参数
164 | {'password','checkPass'} --使用自定义checkPass方法进行校验
165 | }
166 | end
167 |
168 | function _M:checkPass(key)
169 | if self:hasErrors() then return end
170 |
171 | local user = Userinfo.getUserByName(self.username)
172 | if not user then
173 | self:addError('username','账号不存在')
174 | elseif user.password ~= self.password then
175 | self:addError('password','密码错误')
176 | else
177 | self.userinfo = user
178 | end
179 | end
180 |
181 | function _M:login(user)
182 | if not self:validate() then return false end
183 |
184 | return user.login(self.userinfo, rememberMe and 3600*24*30 or 0)
185 | end
186 |
187 | return _M
188 |
189 |
190 | 在Controller里使用Model:
191 |
192 | local model = require('models.LoginForm'):new() --因为Model包含有数据,一定要调用new方法生成新实例,避免出现数据缓存问题
193 |
194 | --通过model的load()方法,可自动装载数据
195 | if model:load(self.request.post()) and model:login(self.user) then
196 | return self:goHome()
197 | end
198 |
199 | 视图(Views)
200 |
201 | 视图基于lua-resty-template(https://github.com/bungle/lua-resty-template)实现
202 |
203 | 在视图中必须使用以下标签:
204 | {{expression}}, 输出expression表达式,并用经过html格式化
205 | {*expression*}, 原样输出expression表达式的结果
206 | {% lua code %}, 执行Lua代码
207 | {# comments #}, 所有{#和#}之间的内容都被认为是注释掉的(即不输出或执行)
208 |
209 | 与lua-resty-template的普通用法不同,所有视图文件,以lua文件的形式保存在views子目录下,文件名为*.lua.html
210 | controller中的render方法渲染试图时,会以require的形式去获取视图内容
211 |
212 | views/layout 目录存放框架视图,渲染视图时默认使用views/layout/main.lua.html,
213 | controller中可通过layout属性设置使用不同的框架视图,如:
214 | local _M = require("vendor.Controller"):new{layout = 'manage'}
215 |
216 | views下的其他子目录分别为不同功能模块对应的内容视图,所有页头,页尾,菜单等内容应在框架视图中实现
217 | 内容视图中不推荐使用{(template)}的形式来包含其他视图,因与默认渲染方式不同,会导致找不到文件等错误
218 |
219 | controller在渲染视图时,会将所有application及controller的数据传递给视图,因此在视图中可以直接使用这些数据
220 | 如:{{lang.siteName}}可输出语言包中配置的网站名称
221 |
222 | 视图中可以通过设置context属性的方式用于内容视图跟框架视图之间传值,如设置:
223 | {% context.title = '开发说明' %}
224 | 则可以在框架视图中{{title}}输出显示
225 |
226 | 获取请求参数
230 |
231 | local request = require("vendor.Request")
232 | // 在Controller方法里可以直接通过self.request调用
233 |
234 | local get = request.get()
235 | // 等价于php: $get = $_GET;
236 |
237 | local id = request.get('id');
238 | // 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : null;
239 |
240 | local id = request.get('id', 1)
241 | // 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : 1;
242 |
243 | local post = request.post()
244 | // 等价于php: $post = $_POST;
245 |
246 | local name = request.post('name')
247 | // 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : null;
248 |
249 | local name = request.post('name', '')
250 | // 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : '';
251 |
252 | local cookie = request.cookie()
253 | // 等价于php: $cookie = $_COOKIE;
254 |
255 | local sso = request.cookie('sso')
256 | // 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : null;
257 |
258 | local sso = request.cookie('sso', '')
259 | // 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : '';
260 |
261 | --判断是否有上传文件
262 | ngx.say(request.isPostFile())
263 | // 注意跟php不同的地方,如果有上传文件时,普通参数是无法通过request.post()方法获取到的
264 | // 建议上传文件时,普通参数通过GET传递
265 |
266 | --接收上传文件
267 | local file = require('vendor.Files')
268 | local savename = path..filename
269 |
270 | local ok,err = file.receive('upfile',savename) --接收name为upfile的上传文件
271 | if not ok then
272 | return {retcode=1,retmsg='接收文件失败,请重试:'..err}
273 | end
274 |
275 |
276 |
277 | 设置cookie:
278 |
279 | local util = require "vendor.Util"
280 | util.set_cookie('abc','123')
281 | util.set_cookie('def','456',3600)
282 | util.set_cookie(name,value,expire,path,domain,secure,httponly)
283 |
284 |
285 | Session操作:
286 |
287 | local session = require "vendor.Session"
288 | // 在Controller方法里可以直接通过self.session调用
289 | --session.start() --启用session,会去设置一个_sessionid的cookie,此操作可以省略
290 | --session.exptime = 1800 --修改session失效时间时,默认30分钟
291 | --session.cache = ngx_shared['session_cache'] --默认使用ngx的缓存,对应runtime/nginx.conf里的lua_shared_dict配置项
292 | --session.cache = require("vendor.Memcache"):new{host="127.0.0.1",port="11211"} --使用memcached缓存打开此配置
293 |
294 | local sessions = session.get()
295 | // 等价于php: $sessions = $_SESSION;
296 |
297 | local abc = session.get('abc')
298 | --等价于:local abc = session.abc
299 | --等价于:local abc = session['abc']
300 | // 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : null;
301 |
302 | local abc = session.get('abc', '')
303 | // 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : '';
304 |
305 | //设置session值
306 | session.set('abc','abc-value')
307 | --等价于:session.abc = 'abc-value'
308 | --等价于:session['abc'] = 'abc-value'
309 |
310 | 数据库访问 (DAO)
314 |
315 | local db = require('vendor.Mysql'):new{
316 | host = "127.0.0.1",
317 | port = 3306,
318 | database = "mytest",
319 | user = "root",
320 | password = "",
321 | charset = "utf8", --建议配置Mysql的默认连接字符集为utf8,可去掉此配置项,此项配置会增加一次'SET NAMES utf8'的操作
322 | }
323 |
324 | 或者直接通过配置文件获得:
325 | local db = require('config.db')
326 |
327 | 执行sql:
328 | local rows = db:query('select * from mytable')
329 | ngx.say(#rows)
330 |
331 | 使用查询生成器 (Query Builder)
332 |
333 | vendor.Query 封装了 vendor.Mysql,并提供快捷构建 安全 查询语句的方法,例如:
334 |
335 | local db = require('config.db')
336 | local query = require('vendor.Query')()
337 | local rows = query.use(db).from('products').where({product_id=123}).all()
338 | --相当于执行local rows = db:query("select * from products where product_id='123'")
339 | ngx.say(query.get('sql')) --能获取到构造出的sql语句
340 |
341 |
342 | --query.use()用于指定链接的数据库
343 | query.use(require('config.db'))
344 |
345 |
346 | --query.select()用于指定要查询的字段,不指定默认为select *
347 | query.select({'id', 'email'})
348 | // 等同于:
349 | query.select('id, email')
350 |
351 |
352 | --query.from()用于指定要查询的表格
353 | // SELECT * FROM `user`
354 | query.from('user')
355 |
356 |
357 | --query.where()用于定义 SQL 语句当中的 WHERE 子句。 你可以使用如下三种格式来定义 WHERE 条件:
358 |
359 | 字符串格式,例如:'status=1' , 这个方法不会自动加引号或者转义。
360 | 哈希格式,例如: {status=1,type=2} ,这个方法将正确地为字段名加引号以及为取值范围转义
361 | 操作符格式,例如:{'in', {'2012','2011'}}
362 |
363 | 操作符格式
364 |
365 | 操作符格式允许你指定类程序风格的任意条件语句,如下所示:
366 |
367 | {操作符, 操作数1, 操作数2, ...}
368 | 其中每个操作数可以是字符串格式、哈希格式或者嵌套的操作符格式, 而操作符可以是如下列表中的一个:
369 |
370 | and: 操作数会被 AND 关键字串联起来。例如,{'and', 'id=1', 'id=2'} 将会生成 id=1 AND id=2。
371 | 如果操作数是一个数组,它也会按上述规则转换成 字符串。例如,{'and', 'type=1', {'or', 'id=1', 'id=2'}}
372 | 将会生成 type=1 AND (id=1 OR id=2)。 这个方法不会自动加引号或者转义。
373 |
374 | or: 用法和 and 操作符类似,这里就不再赘述。
375 |
376 | between: 第一个操作数为字段名称,第二个和第三个操作数代表的是这个字段 的取值范围。例如,{'between', 'id', 1, 10} 将会生成 id BETWEEN 1 AND 10。
377 |
378 | not between: 用法跟between类似
379 |
380 | in: 第一个操作数应为字段名称,第二个操作符既是一个数组。 例如,
381 | {'in', 'id', {1, 2, 3}} 将生成 `id` IN ('1', '2', '3')
382 | 该方法将正确地为字段名加引号以及为取值范围转义
383 |
384 | not in: 用法和 in 操作符类似,这里就不再赘述。
385 |
386 | --query.orderBy() 方法是用来指定 SQL 语句当中的 ORDER BY 子句的。例如,
387 | // ... ORDER BY create_time desc
388 | query.orderBy('create_time desc')
389 | 等价于:query.orderBy{create_time=desc}
390 |
391 |
392 | --query.groupBy() 方法是用来指定 SQL 语句当中的 GROUP BY 片断的。例如,
393 | // ... GROUP BY `id`, `status`
394 | query.groupBy{'id', 'status'}
395 |
396 |
397 | --query.limit() 和 query.offset() 是用来指定 SQL 语句当中 的 LIMIT 和 OFFSET 子句的。例如,
398 | // ... LIMIT 10 OFFSET 20 等价于mysql的limit 20,10
399 | query.limit(10).offset(20)
400 | 如果你指定了一个无效的 limit 或者 offset(例如,一个负数),那么它将会被忽略掉。
401 |
402 |
403 | --query.page() 能通过传递"vendor.Pager"对象,设置 LIMIT 和 OFFSET 达到分页查询的目的:
404 | local page = require("vendor.Pager"){
405 | pageSize = 8,
406 | totalCount = 10,
407 | }
408 | query.page(page) --等价于:query.limit(page.limit).offset(page.offset)
409 |
410 |
411 | 查询方法
412 |
413 | vendor.Query 提供了一整套的用于不同查询目的的方法。
414 |
415 | query.all(): 将返回一个由行组成的数组。
416 | query.one(): 返回结果集的第一行。
417 | query.count(): 返回 COUNT 查询的结果。
418 |
419 | 例如:
420 | local db = require('config.db')
421 | local query = require('vendor.Query'){}.use(db).from('products').where{product_id=123}
422 | local rows = query.all()
423 | local row = query.one()
424 | local count = query.count()
425 |
426 |
427 | 其他方法:
428 | query.insert(): 插入数据。
429 | query.update(): 更新数据。
430 | query.delete(): 删除数据。
431 | query.save(): 新数据调用等价于query.insert(),查询结果集调用等价于query.update()。
432 |
433 | 例如:
434 | local db = require('config.db')
435 | local data = {product_id=123,product_name='name123'}
436 | local query = require('vendor.Query')(data).use(db).from('products')
437 |
438 | --插入:
439 | local row = query.insert()
440 | --要获取新插入的数据的自增长id,请使用:
441 | local id = row.get('insert_id') --等价于query.get('insert_id')
442 |
443 | --更新:
444 | row.product_name = 'name456'
445 | row.update() --或者query.update(row)
446 |
447 | --删除:
448 | row.delete() --或者query.update(row)
449 |
450 | 使用活动记录 (Active Record)
451 |
452 | vendor.ActiveRecord 进一步封装了查询生成器vendor.Query
453 | 同时vendor.ActiveRecord继承于vendor.Model,能够使用模型对象的load(),rules(),hasErrors(),validate()等方法
454 |
455 | 使用示例:
456 | local Product = require('vendor.ActiveRecord'){
457 |
458 | --db = require('config.db'), --可选属性,指定使用的数据库连接,未设置时默认使用'config.db'
459 |
460 | tableName = function() --必须实现tableName方法,返回要操作的数据表名称
461 | return 'products'
462 | end
463 |
464 | }
465 |
466 | --插入数据:
467 | local product = Product.new{
468 | product_id = 123,
469 | product_name = 'name123',
470 | } --返回的对象支持使用vendor.Query方法
471 | product.save() --等价于product.insert()
472 |
473 | --快速查找一行
474 | local row = Product.findOne{product_id = 123} --返回的对象支持使用vendor.Query方法
475 |
476 | --快速查找多行
477 | local rows = Product.findAll{user='creater'} --返回的对象支持使用vendor.Query方法
478 |
479 | --复杂查询
480 | local query = Product.find().where{user='creater'} --返回的对象支持使用vendor.Query方法
481 | local page = Pager{
482 | pageSize = 8,
483 | totalCount = query.count(),
484 | }
485 | local list = query.orderBy('create_time desc').page(page).all()
486 |
487 | --同样可以执行以下操作
488 | query.insert(): 插入数据。
489 | query.update(): 更新数据。
490 | query.delete(): 删除数据。
491 |
492 |
493 | 建议每个数据表格在models目录下建立一个继承于'vendor.ActiveRecord'的操作类
494 |
495 | 数据库安全问题
496 |
497 | 使用vendor.ActiveRecord 或 vendor.Query 自动生成查询语句
498 | 在构造sql语句的过程中,如果传递的参数是表格,构造器会进行转义操作,防止sql注入
499 |
500 | 但如果传递的是字符串,则不会进行转义操作,建议尽量少用,并能确保sql安全
501 | 可以通过 query.get('sql') 获取执行的sql语句
502 |
503 | 如果要进行转义,可使用util.mescape()方法
504 | local util = require("vendor.Util")
505 | value = util.mescape(value)
506 |
507 |