-- ───────────────────────────────────────────────────────────────────────────── -- 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