204 lines
7.4 KiB
Lua
204 lines
7.4 KiB
Lua
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
-- ic-web – Blockdefinition und Pruefung
|
|||
|
|
--
|
|||
|
|
-- Der sicherheitskritische Teil der Resource.
|
|||
|
|
--
|
|||
|
|
-- Seiteninhalte sind NIEMALS HTML, sondern eine Liste typisierter Bloecke.
|
|||
|
|
-- Der Grund: die Seiten werden im NUI von pc-live angezeigt, also im selben
|
|||
|
|
-- Kontext wie der PC selbst. Wer dort Markup einschleusen kann, kann auch
|
|||
|
|
-- dessen NUI-Callbacks aufrufen. Ein Blockmodell schliesst das konstruktiv
|
|||
|
|
-- aus, statt es filtern zu muessen – Filtern von HTML ist ein Wettlauf, den
|
|||
|
|
-- man langfristig verliert.
|
|||
|
|
--
|
|||
|
|
-- Jeder Block wird hier gegen ein festes Schema geprueft. Unbekannte Typen und
|
|||
|
|
-- unbekannte Felder fallen weg, statt durchgereicht zu werden.
|
|||
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
ICWeb = ICWeb or {}
|
|||
|
|
ICWeb.Blocks = {}
|
|||
|
|
|
|||
|
|
local B = ICWeb.Blocks
|
|||
|
|
|
|||
|
|
-- Feldtypen: text (einzeilig), longtext (mehrzeilig), url, int, enum, list
|
|||
|
|
B.SCHEMA = {
|
|||
|
|
heading = {
|
|||
|
|
label = 'Überschrift',
|
|||
|
|
fields = {
|
|||
|
|
text = { kind = 'text', max = 120, required = true },
|
|||
|
|
level = { kind = 'enum', values = { 1, 2, 3 }, default = 2 },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
text = {
|
|||
|
|
label = 'Textabsatz',
|
|||
|
|
fields = {
|
|||
|
|
text = { kind = 'longtext', max = 4000, required = true },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
image = {
|
|||
|
|
label = 'Bild',
|
|||
|
|
fields = {
|
|||
|
|
url = { kind = 'url', max = 500, required = true },
|
|||
|
|
alt = { kind = 'text', max = 160, default = '' },
|
|||
|
|
caption = { kind = 'text', max = 200, default = '' },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
list = {
|
|||
|
|
label = 'Aufzählung',
|
|||
|
|
fields = {
|
|||
|
|
items = { kind = 'list', max = 20, itemMax = 200, required = true },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
button = {
|
|||
|
|
label = 'Schaltfläche',
|
|||
|
|
fields = {
|
|||
|
|
label = { kind = 'text', max = 60, required = true },
|
|||
|
|
target = { kind = 'text', max = 120, required = true },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
contact = {
|
|||
|
|
label = 'Kontaktbox',
|
|||
|
|
fields = {
|
|||
|
|
email = { kind = 'text', max = 200, default = '' },
|
|||
|
|
phone = { kind = 'text', max = 40, default = '' },
|
|||
|
|
address = { kind = 'text', max = 200, default = '' },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
divider = {
|
|||
|
|
label = 'Trennlinie',
|
|||
|
|
fields = {},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- ── Hilfsfunktionen ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
-- Steuerzeichen entfernen und Laenge begrenzen. Winkelklammern bleiben
|
|||
|
|
-- erhalten: der Renderer setzt Text ueber textContent, dort sind sie harmlos
|
|||
|
|
-- und wuerden sonst grundlos verstuemmelt.
|
|||
|
|
local function cleanText(value, maxLen, allowNewlines)
|
|||
|
|
if type(value) ~= 'string' then return nil end
|
|||
|
|
|
|||
|
|
local out = value:gsub('%z', '')
|
|||
|
|
if allowNewlines then
|
|||
|
|
out = out:gsub('[\r]', '')
|
|||
|
|
out = out:gsub('[^\n\32-\126\128-\255]', '')
|
|||
|
|
else
|
|||
|
|
out = out:gsub('[^\32-\126\128-\255]', '')
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
out = out:sub(1, maxLen or 200)
|
|||
|
|
return out
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Bild-URLs: nur https, nur erlaubte Hosts.
|
|||
|
|
--
|
|||
|
|
-- Ohne Hostpruefung landet frueher oder spaeter etwas auf einer IC-Seite, das
|
|||
|
|
-- dort nicht hingehoert. Die Meldefunktion faengt den Rest, aber sie greift
|
|||
|
|
-- erst nachtraeglich.
|
|||
|
|
function B.CheckImageUrl(url)
|
|||
|
|
if type(url) ~= 'string' then return nil, 'keine Adresse' end
|
|||
|
|
|
|||
|
|
url = url:gsub('%s', '')
|
|||
|
|
if #url == 0 or #url > 500 then return nil, 'Adresse zu lang oder leer' end
|
|||
|
|
|
|||
|
|
local host = url:match('^https://([^/]+)/')
|
|||
|
|
if not host then return nil, 'nur https-Adressen erlaubt' end
|
|||
|
|
host = host:lower()
|
|||
|
|
|
|||
|
|
local allowed = (Config and Config.ImageHosts) or {}
|
|||
|
|
for _, pattern in ipairs(allowed) do
|
|||
|
|
-- Exakter Host oder Subdomain davon.
|
|||
|
|
if host == pattern or host:sub(-(#pattern + 1)) == ('.' .. pattern) then
|
|||
|
|
return url
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return nil, ('Bildhost nicht erlaubt: %s'):format(host)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- ── Einzelnen Block pruefen ─────────────────────────────────────────────────
|
|||
|
|
-- Gibt (block, nil) oder (nil, fehlertext) zurueck.
|
|||
|
|
function B.Validate(block)
|
|||
|
|
if type(block) ~= 'table' then return nil, 'Block ist kein Objekt' end
|
|||
|
|
|
|||
|
|
local def = B.SCHEMA[block.type]
|
|||
|
|
if not def then
|
|||
|
|
return nil, ('unbekannter Blocktyp: %s'):format(tostring(block.type))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Neu aufbauen statt kopieren: was nicht im Schema steht, faellt weg.
|
|||
|
|
local out = { type = block.type }
|
|||
|
|
|
|||
|
|
for name, field in pairs(def.fields) do
|
|||
|
|
local raw = block[name]
|
|||
|
|
|
|||
|
|
if field.kind == 'text' or field.kind == 'longtext' then
|
|||
|
|
local value = cleanText(raw, field.max, field.kind == 'longtext')
|
|||
|
|
if value == nil or value == '' then
|
|||
|
|
if field.required then
|
|||
|
|
return nil, ('%s: Feld "%s" fehlt'):format(def.label, name)
|
|||
|
|
end
|
|||
|
|
value = field.default or ''
|
|||
|
|
end
|
|||
|
|
out[name] = value
|
|||
|
|
|
|||
|
|
elseif field.kind == 'url' then
|
|||
|
|
local value, err = B.CheckImageUrl(raw)
|
|||
|
|
if not value then
|
|||
|
|
if field.required then
|
|||
|
|
return nil, ('%s: %s'):format(def.label, err or 'ungueltige Adresse')
|
|||
|
|
end
|
|||
|
|
value = ''
|
|||
|
|
end
|
|||
|
|
out[name] = value
|
|||
|
|
|
|||
|
|
elseif field.kind == 'enum' then
|
|||
|
|
local ok = false
|
|||
|
|
for _, allowed in ipairs(field.values) do
|
|||
|
|
if raw == allowed then ok = true break end
|
|||
|
|
end
|
|||
|
|
out[name] = ok and raw or field.default
|
|||
|
|
|
|||
|
|
elseif field.kind == 'list' then
|
|||
|
|
if type(raw) ~= 'table' then
|
|||
|
|
if field.required then
|
|||
|
|
return nil, ('%s: Liste fehlt'):format(def.label)
|
|||
|
|
end
|
|||
|
|
raw = {}
|
|||
|
|
end
|
|||
|
|
local items = {}
|
|||
|
|
for i = 1, math.min(#raw, field.max) do
|
|||
|
|
local item = cleanText(raw[i], field.itemMax)
|
|||
|
|
if item and item ~= '' then items[#items + 1] = item end
|
|||
|
|
end
|
|||
|
|
if #items == 0 and field.required then
|
|||
|
|
return nil, ('%s: Liste ist leer'):format(def.label)
|
|||
|
|
end
|
|||
|
|
out[name] = items
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return out, nil
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- ── Ganze Seite pruefen ─────────────────────────────────────────────────────
|
|||
|
|
-- Gibt (blockliste, nil) oder (nil, fehlertext) zurueck.
|
|||
|
|
function B.ValidatePage(blocks)
|
|||
|
|
if type(blocks) ~= 'table' then return nil, 'Seiteninhalt ist keine Liste' end
|
|||
|
|
|
|||
|
|
local maxBlocks = (Config and Config.MaxBlocksPerPage) or 60
|
|||
|
|
if #blocks > maxBlocks then
|
|||
|
|
return nil, ('zu viele Bloecke (%d, erlaubt sind %d)'):format(#blocks, maxBlocks)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local out = {}
|
|||
|
|
for i = 1, #blocks do
|
|||
|
|
local block, err = B.Validate(blocks[i])
|
|||
|
|
if not block then
|
|||
|
|
return nil, ('Block %d: %s'):format(i, err)
|
|||
|
|
end
|
|||
|
|
out[#out + 1] = block
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return out, nil
|
|||
|
|
end
|