IC-Webhosting: Domaenen, Webseiten und Postfaecher ueber Anmeldungen
Ein IC-Hostinganbieter im Spiel. Er vergibt Domaenen, richtet je Domaene einen Zugang ein, und die Inhaber pflegen darauf ihre Webseite. Die gesamte Verwaltung haengt an einer Anmeldung, nicht an Job oder Serverrechten. Ein Zugang ist zugleich ein Postfach: wer sich anmeldet, bekommt das Postfach dieser Adresse. Mehrere Personen koennen denselben Zugang benutzen und dasselbe Postfach gleichzeitig offen haben - ein Firmenpostfach gehoert der Firma, nicht einer Person. Drei Ebenen: superadmin vergibt Domaenen und setzt je Domaene einen Admin ein admin pflegt die Seiten seiner Domaene und legt dort Zugaenge an editor pflegt nur die Seiten Seiteninhalte sind kein HTML, sondern typisierte Bloecke. Die Seiten werden im NUI des IC-Computers angezeigt, also im selben Kontext wie dieser selbst - wer dort Markup einschleusen kann, kann auch dessen Callbacks aufrufen. Ein Blockmodell schliesst das konstruktiv aus, statt es filtern zu muessen. Passwoerter liegen gesalzen und gehasht in der Datenbank, gerechnet wird in der Datenbank. Fuenf Fehlversuche je Benutzername sperren den Namen kurzzeitig. Enthaelt README.md mit Einrichtung und Anbindung an den IC-Computer sowie ANLEITUNG.md fuer die Bedienung im Spiel.
This commit is contained in:
commit
a2ce3a69fd
14 changed files with 3018 additions and 0 deletions
203
shared/blocks.lua
Normal file
203
shared/blocks.lua
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue