| 1 | --[[ |
|---|
| 2 | LuCI - Template Parser |
|---|
| 3 | |
|---|
| 4 | Description: |
|---|
| 5 | A template parser supporting includes, translations, Lua code blocks |
|---|
| 6 | and more. It can be used either as a compiler or as an interpreter. |
|---|
| 7 | |
|---|
| 8 | FileId: $Id$ |
|---|
| 9 | |
|---|
| 10 | License: |
|---|
| 11 | Copyright 2008 Steven Barth <steven@midlink.org> |
|---|
| 12 | |
|---|
| 13 | Licensed under the Apache License, Version 2.0 (the "License"); |
|---|
| 14 | you may not use this file except in compliance with the License. |
|---|
| 15 | You may obtain a copy of the License at |
|---|
| 16 | |
|---|
| 17 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 18 | |
|---|
| 19 | Unless required by applicable law or agreed to in writing, software |
|---|
| 20 | distributed under the License is distributed on an "AS IS" BASIS, |
|---|
| 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|---|
| 22 | See the License for the specific language governing permissions and |
|---|
| 23 | limitations under the License. |
|---|
| 24 | |
|---|
| 25 | ]]-- |
|---|
| 26 | |
|---|
| 27 | local fs = require"luci.fs" |
|---|
| 28 | local sys = require "luci.sys" |
|---|
| 29 | local util = require "luci.util" |
|---|
| 30 | local table = require "table" |
|---|
| 31 | local string = require "string" |
|---|
| 32 | local config = require "luci.config" |
|---|
| 33 | local coroutine = require "coroutine" |
|---|
| 34 | |
|---|
| 35 | local tostring, pairs, loadstring = tostring, pairs, loadstring |
|---|
| 36 | local setmetatable, loadfile = setmetatable, loadfile |
|---|
| 37 | local getfenv, setfenv, rawget = getfenv, setfenv, rawget |
|---|
| 38 | local assert, type, error = assert, type, error |
|---|
| 39 | |
|---|
| 40 | --- LuCI template library. |
|---|
| 41 | module "luci.template" |
|---|
| 42 | |
|---|
| 43 | config.template = config.template or {} |
|---|
| 44 | |
|---|
| 45 | viewdir = config.template.viewdir or util.libpath() .. "/view" |
|---|
| 46 | compiledir = config.template.compiledir or util.libpath() .. "/view" |
|---|
| 47 | |
|---|
| 48 | |
|---|
| 49 | -- Compile modes: |
|---|
| 50 | -- memory: Always compile, do not save compiled files, ignore precompiled |
|---|
| 51 | -- file: Compile on demand, save compiled files, update precompiled |
|---|
| 52 | compiler_mode = config.template.compiler_mode or "memory" |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | -- Define the namespace for template modules |
|---|
| 56 | context = util.threadlocal() |
|---|
| 57 | |
|---|
| 58 | --- Manually compile a given template into an executable Lua function |
|---|
| 59 | -- @param template LuCI template |
|---|
| 60 | -- @return Lua template function |
|---|
| 61 | function compile(template) |
|---|
| 62 | local expr = {} |
|---|
| 63 | |
|---|
| 64 | -- Search all <% %> expressions |
|---|
| 65 | local function expr_add(ws1, skip1, command, skip2, ws2) |
|---|
| 66 | table.insert(expr, command) |
|---|
| 67 | return ( #skip1 > 0 and "" or ws1 ) .. |
|---|
| 68 | "<%" .. tostring(#expr) .. "%>" .. |
|---|
| 69 | ( #skip2 > 0 and "" or ws2 ) |
|---|
| 70 | end |
|---|
| 71 | |
|---|
| 72 | -- Save all expressiosn to table "expr" |
|---|
| 73 | template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add) |
|---|
| 74 | |
|---|
| 75 | local function sanitize(s) |
|---|
| 76 | s = "%q" % s |
|---|
| 77 | return s:sub(2, #s-1) |
|---|
| 78 | end |
|---|
| 79 | |
|---|
| 80 | -- Escape and sanitize all the template (all non-expressions) |
|---|
| 81 | template = sanitize(template) |
|---|
| 82 | |
|---|
| 83 | -- Template module header/footer declaration |
|---|
| 84 | local header = 'write("' |
|---|
| 85 | local footer = '")' |
|---|
| 86 | |
|---|
| 87 | template = header .. template .. footer |
|---|
| 88 | |
|---|
| 89 | -- Replacements |
|---|
| 90 | local r_include = '")\ninclude("%s")\nwrite("' |
|---|
| 91 | local r_i18n = '"..translate("%1","%2").."' |
|---|
| 92 | local r_i18n2 = '"..translate("%1", "").."' |
|---|
| 93 | local r_pexec = '"..(%s or "").."' |
|---|
| 94 | local r_exec = '")\n%s\nwrite("' |
|---|
| 95 | |
|---|
| 96 | -- Parse the expressions |
|---|
| 97 | for k,v in pairs(expr) do |
|---|
| 98 | local p = v:sub(1, 1) |
|---|
| 99 | v = v:gsub("%%", "%%%%") |
|---|
| 100 | local re = nil |
|---|
| 101 | if p == "+" then |
|---|
| 102 | re = r_include:format(sanitize(string.sub(v, 2))) |
|---|
| 103 | elseif p == ":" then |
|---|
| 104 | if v:find(" ") then |
|---|
| 105 | re = sanitize(v):gsub(":(.-) (.*)", r_i18n) |
|---|
| 106 | else |
|---|
| 107 | re = sanitize(v):gsub(":(.+)", r_i18n2) |
|---|
| 108 | end |
|---|
| 109 | elseif p == "=" then |
|---|
| 110 | re = r_pexec:format(v:sub(2)) |
|---|
| 111 | elseif p == "#" then |
|---|
| 112 | re = "" |
|---|
| 113 | else |
|---|
| 114 | re = r_exec:format(v) |
|---|
| 115 | end |
|---|
| 116 | template = template:gsub("<%%"..tostring(k).."%%>", re) |
|---|
| 117 | end |
|---|
| 118 | |
|---|
| 119 | return loadstring(template) |
|---|
| 120 | end |
|---|
| 121 | |
|---|
| 122 | --- Render a certain template. |
|---|
| 123 | -- @param name Template name |
|---|
| 124 | -- @param scope Scope to assign to template (optional) |
|---|
| 125 | function render(name, scope) |
|---|
| 126 | return Template(name):render(scope or getfenv(2)) |
|---|
| 127 | end |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | -- Template class |
|---|
| 131 | Template = util.class() |
|---|
| 132 | |
|---|
| 133 | -- Shared template cache to store templates in to avoid unnecessary reloading |
|---|
| 134 | Template.cache = setmetatable({}, {__mode = "v"}) |
|---|
| 135 | |
|---|
| 136 | |
|---|
| 137 | -- Constructor - Reads and compiles the template on-demand |
|---|
| 138 | function Template.__init__(self, name) |
|---|
| 139 | local function _encode_filename(str) |
|---|
| 140 | |
|---|
| 141 | local function __chrenc( chr ) |
|---|
| 142 | return "%%%02x" % string.byte( chr ) |
|---|
| 143 | end |
|---|
| 144 | |
|---|
| 145 | if type(str) == "string" then |
|---|
| 146 | str = str:gsub( |
|---|
| 147 | "([^a-zA-Z0-9$_%-%.%+!*'(),])", |
|---|
| 148 | __chrenc |
|---|
| 149 | ) |
|---|
| 150 | end |
|---|
| 151 | |
|---|
| 152 | return str |
|---|
| 153 | end |
|---|
| 154 | |
|---|
| 155 | self.template = self.cache[name] |
|---|
| 156 | self.name = name |
|---|
| 157 | |
|---|
| 158 | -- Create a new namespace for this template |
|---|
| 159 | self.viewns = context.viewns |
|---|
| 160 | |
|---|
| 161 | -- If we have a cached template, skip compiling and loading |
|---|
| 162 | if self.template then |
|---|
| 163 | return |
|---|
| 164 | end |
|---|
| 165 | |
|---|
| 166 | -- Enforce cache security |
|---|
| 167 | local cdir = compiledir .. "/" .. sys.process.info("uid") |
|---|
| 168 | |
|---|
| 169 | -- Compile and build |
|---|
| 170 | local sourcefile = viewdir .. "/" .. name |
|---|
| 171 | local compiledfile = cdir .. "/" .. _encode_filename(name) .. ".lua" |
|---|
| 172 | local err |
|---|
| 173 | |
|---|
| 174 | if compiler_mode == "file" then |
|---|
| 175 | local tplmt = fs.mtime(sourcefile) or fs.mtime(sourcefile .. ".htm") |
|---|
| 176 | local commt = fs.mtime(compiledfile) |
|---|
| 177 | |
|---|
| 178 | if not fs.mtime(cdir) then |
|---|
| 179 | fs.mkdir(cdir, true) |
|---|
| 180 | fs.chmod(fs.dirname(cdir), "a+rxw") |
|---|
| 181 | end |
|---|
| 182 | |
|---|
| 183 | assert(tplmt or commt, "No such template: " .. name) |
|---|
| 184 | |
|---|
| 185 | -- Build if there is no compiled file or if compiled file is outdated |
|---|
| 186 | if not commt or (commt and tplmt and commt < tplmt) then |
|---|
| 187 | local source |
|---|
| 188 | source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm") |
|---|
| 189 | |
|---|
| 190 | if source then |
|---|
| 191 | local compiled, err = compile(source) |
|---|
| 192 | |
|---|
| 193 | fs.writefile(compiledfile, util.get_bytecode(compiled)) |
|---|
| 194 | fs.chmod(compiledfile, "a-rwx,u+rw") |
|---|
| 195 | self.template = compiled |
|---|
| 196 | end |
|---|
| 197 | else |
|---|
| 198 | assert( |
|---|
| 199 | sys.process.info("uid") == fs.stat(compiledfile, "uid") |
|---|
| 200 | and fs.stat(compiledfile, "mode") == "rw-------", |
|---|
| 201 | "Fatal: Cachefile is not sane!" |
|---|
| 202 | ) |
|---|
| 203 | self.template, err = loadfile(compiledfile) |
|---|
| 204 | end |
|---|
| 205 | |
|---|
| 206 | elseif compiler_mode == "memory" then |
|---|
| 207 | local source |
|---|
| 208 | source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm") |
|---|
| 209 | if source then |
|---|
| 210 | self.template, err = compile(source) |
|---|
| 211 | end |
|---|
| 212 | |
|---|
| 213 | end |
|---|
| 214 | |
|---|
| 215 | -- If we have no valid template throw error, otherwise cache the template |
|---|
| 216 | if not self.template then |
|---|
| 217 | error(err) |
|---|
| 218 | else |
|---|
| 219 | self.cache[name] = self.template |
|---|
| 220 | end |
|---|
| 221 | end |
|---|
| 222 | |
|---|
| 223 | |
|---|
| 224 | -- Renders a template |
|---|
| 225 | function Template.render(self, scope) |
|---|
| 226 | scope = scope or getfenv(2) |
|---|
| 227 | |
|---|
| 228 | -- Put our predefined objects in the scope of the template |
|---|
| 229 | setfenv(self.template, setmetatable({}, {__index = |
|---|
| 230 | function(tbl, key) |
|---|
| 231 | return rawget(tbl, key) or self.viewns[key] or scope[key] |
|---|
| 232 | end})) |
|---|
| 233 | |
|---|
| 234 | -- Now finally render the thing |
|---|
| 235 | local stat, err = util.copcall(self.template) |
|---|
| 236 | if not stat then |
|---|
| 237 | error("Error in template %s: %s" % {self.name, err}) |
|---|
| 238 | end |
|---|
| 239 | end |
|---|