Initial commit

This commit is contained in:
mid 2024-06-01 17:40:11 +03:00
commit f4b0a0b531
36 changed files with 4027 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
objd/
objkey
cfg.lua

7
404.html.l Normal file
View File

@ -0,0 +1,7 @@
{% title = BigGlobe.cfg.sitename .. " - 404" %}
{% function content() %}
<p>This page doesn't exist.</p>
{% end %}
{# base.inc

56
README Normal file
View File

@ -0,0 +1,56 @@
This document is intended for people looking to contribute to Ikibooru. For Ikibooru users, check the official webpage: https://mid.net.ua/ikibooru.html
Ikibooru uses Pegasus.lua as a barebones HTTP library, despite the latter's intention to be larger in scope. This is because it fails at this task. As such, Ikibooru's version of Pegasus.lua is also modified to include additional features:
1. Support for multipart/form-data requests (parseMultipartFormData @ pegasus/request.lua);
2. set FD_CLOEXEC on each socket using luaposix;
3. more specific logging;
4. removed a really stupid feature where it tries to write to a response already written, if the status code is 404.
(UNDONE) 5. Request handlers are placed in coroutines and run in a round-robin fashion, so as to make sure one request or response wouldn't block other users
The first is necessary for file uploads to be possible.
The second is necessary as Ikibooru calls upon the shell for a few things, such as generating a thumbnail or sending an e-mail. Otherwise child processes inherit open file descriptors, which causes clients to wait until any internal processes end.
Yes, it calls upon the shell. This quirk is why Ikibooru currently supports only Unices running Bourne-like shells. The offending lines appear in smtpauth.lua and db.lua. Protection against command injection is done by wrapping potentially untrusted input with single quotes, the contents of which the shell should interpret literally. Single quotes are beforehand turned into '"'"'. Behaviour should be equivalent to shlex.quote from the Python standard library. This on paper should achieve perfect command injection prevention, but more testing is required. More on this later.
The aptly named html.lua handles formatting such as escaping or unescaping, and so must also be tested for security. The exception is SQL escaping which is done by LuaSQL (which wraps mysql_real_escape_string).
db.lua handles all SQL access and filesystem access for storing individual object files.
db.lua also handles valid formats for usernames, object names, emails, etc. For example, object names are not permitted to be CON, PRN, AUX, LPT1...LPT9, etc., due to these names being reserved by Windows (testobjfilename @ db.lua). It also makes sure a filename does not contain slashes, backslashes, unprintable ASCII characters, any other reserved filenames, may not consist only of dots, etc. These should make it more difficult to perform any command injection.
Maybe valid names should be handled by html.lua?
Ikibooru encodes object IDs using AES-128-ECB so as to hide their chronology. To do this it generates a random 16-byte string during installation, which it will use as a key forever. The security of object IDs is minor and its disclosure isn't as catastrophic as, say, the bypassing of user verification. The use of ECB is deliberate as it does not require an IV, and having unique plaintext-ciphertext pairs is useful. In other words, AES is not used in such a way that the choice of ECB will be detrimental to security. However, it is extremely important this key is not lost or changed, otherwise all external links to objects will be suddenly invalidated.
db.lua makes the distinction of virtual and physical filenames (terminology borrowed from the virtual and physical memory of computer architectures). Virtual filenames are those shown to the end user (/objd/ABCD1234..ABCD1234), whereas physical filenames are those used internally (objd/AB/CD/12/34/../AB/CD/12/34). Ikibooru is designed to organize the filesystem in such a manner so as to minimize any penalties from having too many entries in any one directory.
main.lua is where the server begins running. All requests caught by Pegasus.lua are passed to the callback function. Within it, a few special paths are passed to specific codepaths, and the rest go to the template system. Make no mistake, the templates do more than formatting a page. They may also handle and call to perform serious operations.
Note, that the generic template path is wrapped with a pcall to catch errors. This lets the server keep running, should anything occur. The same is not done for the special paths, such as /verif, /addobj, etc. Therefore, making those parts extra error-resistant is more important.
Ikibooru uses Lyre to compile templates, and its done once before Pegasus.lua is first initialized. Prior to passing to Lyre, Ikibooru preprocesses lines starting with {# to implement a basic "inclusion" system. Currently, all templates at the end include base.inc, which grants them a standard layout with a header, sign-in/out button, etc.
BigGlobe handles the main configuration. The name is composed of Big (which means "great" or "powerful") and Globe (which means global). The name has nothing to do with flat-earthers nor globe-earthers.
Sessions are done client-side, similarly to how JWT works. A verification link is sent to the e-mail address, which contains the session token. The token is composed of a few pieces of data concatenated with their HMAC-SHA256 digest. The session key used for the HMAC is 64 bytes long and is generated per each server launch. It is good practice to restart the server every now and then to invalidate all sessions, also rendering nil any efforts to bruteforce the session key. Assuming perfect security, "every now and then" means every few thousand years, but let us say every few months just in case.
Notice that only an HMAC is used. There is no encryption, which means no secrecy, only integrity. So far this has been good enough.
While Ikibooru can run on just about any Unix-like system, an automatic installer exists only for Linux. More would be welcome.
obje.html.l is arguably the ugliest part of the entire program, in part due to the unreadable JS code within. The problem is because Pegasus.lua deals with only one request at a time. Should a large file be uploaded (say 50MB), it will effectively block the server from doing anything else. This is unacceptable, so JS is used to upload files in chunks of 512KB. Requests from other users inbetween can then be serviced. Now obje does the following upon submitting a form:
1. Send a POST request to delete appropriately marked files
2. If there are new files, send a POST request to create empty files of specific sizes (to make sure such sizes are allowed in the first place)
2.5. Send a POST request for each 512KB chunk of every file, which the server then places at the appropriate offset
3. Allow the rest of the form to be submitted naturally
What makes the code unreadable (IMO) is the asynchronous nature. It's hard to tell the overall control flow what with all of the onreadystatuschange events.
Later on I realized that large responses (file downloads) block the server, also. Don't know why it took so long, but that's what lead to change no. 5 of Pegasus.lua. I still kept the uploading JS code in obje.html.l, because the page needed JS before that, anyway, and it provided better UX.
Unfortunately, mod no. 5 cut down processing speed to around 66% of what it was without, and considering that Ikibooru is already meant to be used together with a reverse proxy, since that proxy likely supports accelerated static file serving (X-Sendfile or X-Accel-Redirect) the mod was ultimately undone. Perhaps it should be made optional?
Times are stored in the database as DATETIMEs in the UTC time zone. I don't remember why this was done. I suppose because the TIMESTAMP datatype (which uses UTC automatically) breaks after 2038, whereas DATETIME can go upto the year 9999. The server passes to browsers times in UTC (strings directly from the DB engine), and then clientside JS code proceeds to shift them into the the user's appropriate local timezone (static/datetimes.js). This means that should JS be disabled, all times will be displayed as UTC.
main.lua and core.lua are split to later separate Ikibooru the "service", from Ikibooru the "server". Some reverse proxies can create Lua environments of their own and have Ikibooru attached to them directly (skipping any inter-server communication overhead), That way, Ikibooru wouldn't have to run as an HTTP server at all, but this isn't supported yet.

157
admin.html.l Normal file
View File

@ -0,0 +1,157 @@
{%
-- ADMIN ONLY
if verified and verified.privs ~= 255 then verified = nil end
if verified then
local pohst = request:post()
if pohst and pohst.csrf and DB.csrfverify(verified.id, Escapes.urlunescape(tostring(pohst.csrf))) then
-- Basic settings
if pohst["sitename"] and not Escapes.urlunescape(pohst["sitename"]):match"^%s*$" then
BigGlobe.cfg.sitename = Escapes.urlspunescape(pohst["sitename"])
end
BigGlobe.cfg.enable18plus = pohst["enable18plus"] ~= nil
-- Tag categories
for i=1,BigGlobe.cfg.tc.n do
if pohst["tcn" .. i] then
BigGlobe.cfg.tc[i].name = Escapes.urlspunescape(pohst["tcn" .. i])
end
if pohst["tcc" .. i] then
BigGlobe.cfg.tc[i].col = tonumber(Escapes.urlunescape(pohst["tcc" .. i]):sub(2), 16) or 0
end
end
-- New tag
if pohst["ntn"] and tonumber(pohst["ntc"]) then
local cat = tonumber(pohst["ntc"])
if cat >= 1 and cat <= BigGlobe.cfg.tc.n then
DB.tagadd(Escapes.urlspunescape(pohst["ntn"]), cat, pohst["nta"] ~= nil)
end
end
-- Tag editing
if pohst["ettags"] and (#pohst["ettags"] > 0) then
local tagarr = {}
for tagstr in pohst["ettags"]:gmatch("%d+") do
table.insert(tagarr, tonumber(tagstr))
end
local newname = pohst["etname"] and Escapes.urlspunescape(pohst["etname"])
if newname and newname:match"^%s*$" then newname = nil end
local newcategory = tonumber(pohst["etcat"])
local newadultonly = pohst["eta"] ~= nil
local del = pohst["etdel"]
if del and del:match"^%s*$" then del = nil end
DB.updatetags(tagarr, newname, newcategory, newadultonly, del)
end
-- Mods
if pohst["mods"] then
local newmods = {}
for m in Escapes.urlspunescape(pohst["mods"]):gmatch"[^,]+" do
if not m:match("^%s*$") then
table.insert(newmods, m)
end
end
DB.setmodsviaemails(newmods)
end
-- Ruleset
if pohst["ruleset"] then
BigGlobe.cfg.ruleset = Escapes.urlspunescape(pohst["ruleset"])
end
-- X-Sendfile
if pohst["sendfilehdr"] then
BigGlobe.cfg.sendfile = Escapes.urlunescape(pohst["sendfilehdr"])
end
if pohst["sendfileprefix"] then
BigGlobe.cfg.sendfileprefix = Escapes.urlunescape(pohst["sendfileprefix"])
end
BigGlobe.sv()
end
else
response:statusCode(403)
end
%}
{% title = "Administrative Settings" %}
{% function content() %}
{% if verified then %}
<form method="POST" action="/admin" id="adminform">
<input type="hidden" name="csrf" value="{{ Escapes.htmlescape(DB.csrf(verified.id)) }}" />
<h1>{{ title }} <input type="submit" value="Submit" style="vertical-align:middle;" /></h1>
<div style="display:inline-block;width:60%;vertical-align:top;">
<h2>Basic Site Settings</h2>
<p style="margin-bottom:0;">Site Name</p>
<input type="text" name="sitename" autocomplete="off" value="{{ Escapes.htmlescape(BigGlobe.cfg.sitename) }}" style="display:block;margin-bottom:1em;" />
<input id="enable18plusparam" type="checkbox" name="enable18plus" {% if BigGlobe.cfg.enable18plus then %}checked {% end %}/>
<label for="enable18plusparam">Enable 18+</label>
<h2>New Tag</h2>
<p style="margin-bottom:0;">Set category ID</p>
<input type="number" name="ntc" min="1" max="{{ BigGlobe.cfg.tc.n }}" />
<p style="margin-bottom:0;">Set name</p>
<input type="text" name="ntn" autocomplete="off" />
<div style="margin-top:1em;"><input type="checkbox" name="nta" id="ntaparam" /><label for="ntaparam">18+</label></div>
<h2>Edit Tag</h2>
<div data-over18="1" style="position:relative;width:30%;" class="tagbox" data-formid="adminform" data-formparaminputid="ettagsparam">
<p>Tags to edit...</p>
<span style="position:relative;min-width:4px;left:0;" contenteditable></span>
</div>
<div class="autocomplete" style="visibility:hidden;width:30%;"></div>
<input type="hidden" id="ettagsparam" name="ettags" value="" />
<input type="number" name="etcat" min="1" max="{{ BigGlobe.cfg.tc.n }}" placeholder="Set category..." style="display:block;margin-bottom:1em;" />
<input type="text" name="etname" autocomplete="off" placeholder="Set name..." style="display:block;margin-bottom:1em;" />
<div style="margin-bottom:1em;"><input type="checkbox" name="eta" id="etaparam" /><label for="etaparam">18+</label></div>
<input type="checkbox" name="etdel" id="etdelbox" /><label for="etdelbox">Delete</label>
<h2>Moderators</h2>
<p>Enter a comma-separated list of e-mails that shall be granted moderator permissions.</p>
{% local mods = DB.getmoderators() %}
<textarea name="mods" style="width:80%;height:10em;">{% for _, m in pairs(mods) do %}{{ Escapes.htmlescape(m.email) }},{% end %}</textarea>
<h2>Ruleset</h2>
<p>The following HTML <strong>(!)</strong> will be displayed upon registration and user reporting. Use this to define a ruleset for your community. Tip: place an &lt;ol&gt; element to establish a clear numbered list.</p>
<textarea name="ruleset" class="uf" style="width:80%;"></textarea>
<h2>File download acceleration</h2>
<p>Using this feature is highly recommended as it lets Ikibooru outsource IO-intensive file downloads onto the reverse proxy, which has ways of speeding up the process. Otherwise large file downloads could freeze the entire website. Said proxy must also be configured to support the feature (it is typically called X-Sendfile or X-Accel-Redirect).</p>
<input type="text" name="sendfilehdr" autocomplete="off" placeholder="HTTP Header" value="{{ Escapes.htmlescape(BigGlobe.cfg.sendfile) }}" style="display:block;margin-bottom:1em;" />
<input type="text" name="sendfileprefix" autocomplete="off" placeholder="Path Prefix" value="{{ Escapes.htmlescape(BigGlobe.cfg.sendfileprefix) }}" style="display:block;margin-bottom:1em;" />
</div>
<div style="display:inline-block;width:30%">
<h2>Tag Categories</h2>
<p>These exist for ease within searching.</p>
<table>
<tr><td>Id</td><td>Name</td><td>Color</td></tr>
{% for i=1,BigGlobe.cfg.tc.n do %}
<tr>
<td>{{ i }}</td>
<td><input type="text" name="tcn{{ i }}" value="{{ Escapes.htmlescape(BigGlobe.cfg.tc[i].name) }}" autocomplete="off" /></td>
<td><input type="color" name="tcc{{ i }}" value="#{{ string.format("%06x", BigGlobe.cfg.tc[i].col) }}" /></td>
</tr>
{% end %}
</table>
</div>
</form>
<script src="/static/tagbox.js"></script>
{% else %}
<p>You are not authorized to view this page.</p>
{% end %}
{% end %}
{# base.inc

56
base.inc Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html class="notranslate" translate="no" lang="en">
<head>
<meta charset="UTF-8">
<meta name="google" content="notranslate" />
<title>{{ Escapes.htmlescape(title) }}</title>
<link rel="stylesheet" href="/static/style.css" type="text/css" />
<link rel="stylesheet" href="/static/tagcats.css" type="text/css" />
<link rel="stylesheet" href="/static/user.css" type="text/css" />
<link rel="icon" type="image/png" href="{{ Escapes.htmlescape(BigGlobe.cfg.domain .. "/static/favicon.png") }}" />
<meta property="og:title" content="{{ Escapes.htmlescape(title) }}" />
<meta property="og:image" content="{{ Escapes.htmlescape(ogImage and ogImage or (BigGlobe.cfg.domain .. "/static/logo.png")) }}" />
<meta property="og:type" content="website" />
</head>
<body>
<header>
<p style="position:absolute;left:1em;margin:0;"><a href="/">{{ Escapes.htmlescape(BigGlobe.cfg.sitename) }}</a></p>
{% if verified and verified.privs >= DB.USER_PRIVS_ADMIN then %}
<p style="position:absolute;left:1em;top:1em;margin:0;"><a href="/admin">Admin Settings</a></p>
{% end %}
{% if verified and verified.privs >= DB.USER_PRIVS_MOD then %}
<p style="position:absolute;left:1em;top:2em;margin:0;"><a href="/apprq">Approval Queue</a></p>
<p style="position:absolute;left:1em;top:3em;margin:0;"><a href="/reports">Reports</a></p>
{% end %}
<div style="position:absolute;right:1em;margin:0;">
{% if verified then %}
<label style="margin:0;">{{ Escapes.htmlescape(verified.displayname) }}</label>
<form method="GET" action="/verif" style="display:inline-block;">
<input type="hidden" name="q" value="" />
<input type="submit" value="Log out" />
</form>
<div><a href="/user/{{ verified.id }}">My profile</a></div>
{% if
(BigGlobe.cfg.membexcl == BigGlobe.MEMBEXCL_MEMBERS_INVITE and verified.privs >= DB.USER_PRIVS_APPROVED)
or (BigGlobe.cfg.membexcl == BigGlobe.MEMBEXCL_MODS_INVITE and verified.privs >= DB.USER_PRIVS_MOD)
or (BigGlobe.cfg.membexcl == BigGlobe.MEMBEXCL_ADMIN_INVITES and verified.privs >= DB.USER_PRIVS_ADMIN) then %}
<div><a href="/invite">Invite</a></div>
{% end %}
{% else %}
<form method="GET" action="/login">
<input type="{{BigGlobe.cfg.anarchy == "ANARCHY" and "text" or "email"}}" name="z" required style="margin-bottom:0.5em;" />
<input type="submit" value="Log in" /><br />
<input type="radio" name="type" id="logemail" value="email" checked style="margin-left:0;" /><label for="logemail">E-mail</label>
<!--<input type="radio" name="type" id="logxmpp" value="xmpp" required /><label for="logxmpp">Jabber</label>-->
</form>
{% end %}
</div>
</header>
<main>{% content() %}</main>
<footer>
<p>Running Ikibooru v0.1</p>
</footer>
<script defer src="/static/datetimes.js"></script>
</body>
</html>

70
bigglobe.lua Normal file
View File

@ -0,0 +1,70 @@
local Escapes=require"html"
local cfg
local function ld()
cfg = loadfile("cfg.lua", "t")
if cfg then cfg = cfg() end
if not cfg then
error("Configuration file is missing.")
end
end
local function serialize(t)
if type(t) == "table" then
local r = {"{"}
for k, v in pairs(t) do
table.insert(r, "[" .. string.format("%q", k) .. "]=" .. serialize(v) .. ",")
end
table.insert(r, "}")
return table.concat(r)
else
return string.format("%q", t)
end
end
local function sv()
local out = io.open("cfg.lua", "wb")
out:write"-- THIS FILE IS AUTO-GENERATED. USE THE WEB ADMIN MENU!!\nreturn "
out:write(serialize(cfg))
out:close()
out = io.open("static/tagcats.css", "wb")
for i=1,cfg.tc.n do
out:write("div.tag.tc" .. (i - 1) .. "::before{content:" .. Escapes.cssescape(cfg.tc[i].name:sub(1, 1)) .. ";}")
out:write("div.tag.tc" .. (i - 1) .. ":hover::before{content:" .. Escapes.cssescape(cfg.tc[i].name) .. ";}")
local r = cfg.tc[i].col >> 16
local g = (cfg.tc[i].col >> 8) & 0xFF
local b = cfg.tc[i].col & 0xFF
local off = 150
local darker = math.floor(math.min(b + off, 255)) | (math.floor(math.min(g + off, 255)) << 8) | (math.floor(math.min(r + off, 255)) << 16)
out:write("div.tag.tc" .. (i - 1) .. "{border-color:#" .. string.format("%06x", cfg.tc[i].col) .. ";background-color:#"..string.format("%06x",darker)..";}")
end
out:close()
end
ld()
sv()
return {
MEMBEXCL_ADMIN_INVITES = 1,
MEMBEXCL_MODS_INVITE = 2,
MEMBEXCL_MEMBERS_INVITE = 3,
MEMBEXCL_PUBLIC_WITHAPPROVAL = 4,
MEMBEXCL_PUBLIC_NOAPPROVAL = 5,
MAX_COMMENT_SIZE = 1024,
REPORT_STATUS_OPEN = 0,
REPORT_STATUS_CLOSED_WONTFIX = 1,
REPORT_STATUS_CLOSED_FIXED = 2,
cfg = cfg,
ld = ld,
sv = sv
}

186
core.lua Normal file
View File

@ -0,0 +1,186 @@
local Lyre = require"lyre"
local Mime = require"mimetypes"
local BigGlobe = require"bigglobe"
local DB = require"db"
local Escapes = require"html"
local LFS = require"lfs"
local SMTPAuth = require"smtpauth"
-- Add template inclusions to Lyre
local function compiel(fn, sub)
local f = io.open(fn, "rb")
local buf = {}
for l in f:lines() do
local includie = l:match("^{#%s*(.*)$")
if includie then
table.insert(buf, compiel(includie, true))
else
table.insert(buf, l)
end
end
f:close()
return sub and table.concat(buf, "\n") or Lyre.compile(table.concat(buf, "\n"), fn)
end
local Static = {}
local Templates = {
["/"] = compiel("index.html.l"),
["/search"] = compiel("search.html.l"),
["/obji"] = compiel("obj.html.l"),
["/obje"] = compiel("obje.html.l"),
["/login"] = compiel("login.html.l"),
["/admin"] = compiel("admin.html.l"),
["/user"] = compiel("user.html.l"),
["/invite"] = compiel("invite.html.l"),
["/reportuser"] = compiel("reportuser.html.l"),
["/reg"] = compiel("reg.html.l"),
["/reports"] = compiel("reports.html.l"),
["404"] = compiel("404.html.l"),
}
local handler = function(req, res)
local verified = req:cookie"sesh" and DB.userverify(Escapes.urlunescape(req:cookie"sesh"))
if verified and verified.privs == DB.USER_PRIVS_BANNED then
verified = nil
end
if req:path() == "/autocomp" then
res:contentType("text/plain;charset=UTF-8")
res:write(DB.autocomplete(Escapes.urlunescape(req.querystring.q), req.querystring.a ~= nil))
elseif req:path():match"^/addobj/?$" then
if verified then
local oid = DB.regnewobj(verified.id, verified.privs >= DB.USER_PRIVS_APPROVED, false)
if oid then
res:addHeader("Location", "/obje/" .. DB.objhideid(oid))
res:statusCode(303)
end
else
res:statusCode(403)
end
res:write""
elseif req:path():match"^/verif/?$" then
res:addHeader("Set-Cookie", "sesh=" .. (req.querystring.q or "") .. "; SameSite=Lax; Secure; HttpOnly")
res:addHeader("Location", "/")
res:statusCode(303)
res:write""
elseif req:path():match"^/apprq/?$" then
if verified and verified.privs >= DB.USER_PRIVS_MOD then
-- Has decided on something already?
if req.querystring.csrf and DB.csrfverify(verified.id, Escapes.urlunescape(req.querystring.csrf)) then
local oid = DB.objshowid(Escapes.urlunescape(req.querystring.oid))
if req.querystring.deliamsure and DB.getuserbyid(DB.getobj(oid).owner).privs < verified.privs then
if req.querystring.banuser then
DB.banuser(DB.getobj(oid).owner)
end
DB.delobj(oid, req.querystring.delfiles ~= nil)
elseif req.querystring.approveiamsure then
if req.querystring.approveuser then
DB.approveuser(DB.getobj(oid).owner)
end
DB.approveobj(oid)
end
end
local oid = DB.nexttoapproveobj()
res:addHeader("Location", oid and ("/obji/" .. DB.objhideid(oid)) or "/")
res:statusCode(303)
else
res:statusCode(403)
end
res:write""
elseif req:path():match"^/static/" or req:path():match"^/objd/" or req:path() == "/favicon.ico" then
local p = req:path()
res:contentType(Mime.guess(p:sub(2):lower()) or "text/plain")
-- Force download
if p:match"^/objd/" then
res:addHeader("Content-Disposition", "attachment;filename=\"" .. p:match"^/objd/[^/]+/(.*)$" .. "\"")
end
res:addHeader("Cache-Control", "max-age=604800")
if req:method() == "HEAD" then
res:write""
return
end
local phys = DB.urltophysical(p)
if not BigGlobe.cfg.sendfile:match"^%s*$" then
res:addHeader(BigGlobe.cfg.sendfile, BigGlobe.cfg.sendfileprefix .. "/" .. phys)
res:write""
else
local f = io.open(phys, "rb")
if f then
local sz = f:seek("end")
if req:headers()["range"] then
local starto, endo = req:headers()["range"]:match("^bytes%=(%d+)%-(%d*)$")
starto = tonumber(starto)
endo = tonumber(endo)
if not endo then
endo = sz - 1
elseif endo >= sz then
res:statusCode(416)
res:write""
return
end
if starto and endo and starto <= endo then
res:statusCode(206)
res:addHeader("Accept-Ranges", "bytes")
res:addHeader("Content-Length", endo - starto + 1)
res:addHeader("Content-Range", "bytes " .. starto .. "-" .. endo .. "/" .. sz)
f:seek("set", starto)
res:write(f:read(endo - starto + 1))
end
else
f:seek("set")
res:write(f:read"*a")
end
f:close()
else
res:statusCode(404)
res:write""
end
end
else
local tmpl = Templates[req:path():sub(1, req:path():sub(2):find("/") or -1)]
if not tmpl then
res:statusCode(404)
tmpl = Templates["404"]
end
res:statusCode(200)
local env = {BigGlobe = BigGlobe, SMTPAuth = SMTPAuth, DB = DB, Escapes = Escapes, LFS = LFS, request = req, response = res, print = print, verified = verified}
local succ, val
if _ENV then
succ, val = xpcall(Lyre.render, debug.traceback, tmpl, env)
else
succ, val = pcall(Lyre.render, tmpl, env)
end
if succ then
res:write(val)
else
print("Error!")
print(val)
res:statusCode(500)
res:write"Internal server error. Try not doing that."
end
end
end
return handler

822
db.lua Normal file
View File

@ -0,0 +1,822 @@
local SQL = require"luasql.mysql"
local BigGlobe = require"bigglobe"
local SQLEnv = SQL.mysql()
local SQLConn
local Rand = require"openssl.rand"
assert(Rand.ready())
local SessionsKey = Rand.bytes(64)
local ObjHideKey = io.open("objkey", "rb"):read"*a"
local AES128ECB = require"openssl.cipher".new("AES-128-ECB") -- Can be ECB without reprecussions
local Escapes = require"html"
local LFS = require"lfs"
local unistd = require"posix.unistd"
local stdio = require"posix.stdio"
local USER_PRIVS_BANNED = 0
local USER_PRIVS_UNAPPROVED = 1
local USER_PRIVS_APPROVED = 2
local USER_PRIVS_MOD = 128
local USER_PRIVS_ADMIN = 255
local function pingdb()
if not SQLConn or not SQLConn:ping() then
SQLConn = SQLEnv:connect("ikibooru", BigGlobe.cfg.sqlu, BigGlobe.cfg.sqlp, BigGlobe.cfg.sqlh)
end
return SQLConn ~= nil
end
-- UTC to store in database
local function getnow()
return "'" .. SQLConn:escape(os.date("!%Y-%m-%d %H:%M:%S")) .. "'"
end
local function autocomplete(liek, over18)
pingdb()
local ret = {}
local cursor = SQLConn:execute("SELECT * FROM tags WHERE name LIKE '" .. SQLConn:escape("%" .. liek .. "%") .. "'" .. (over18 and "" or " AND adultonly = 0") .. ";")
if not cursor then return "" end
local row = cursor:fetch({})
while row do
ret[#ret + 1] = table.concat(row, ",") .. "\n"
row = cursor:fetch(row)
end
cursor:close()
return table.concat(ret)
end
local function searchobjs(taglist, namefilter, offset, adultstuff)
-- Security check
for k,v in pairs(taglist) do
if type(taglist[k]) ~= "number" then
return nil
end
end
if not adultstuff then
adultstuff = -1
end
pingdb()
offset = tonumber(offset)
local sqlq
if #taglist > 0 and namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s id IN (SELECT objid FROM objtag WHERE tagid IN (%s) GROUP BY objid HAVING COUNT(tagid) >= %s) AND MATCH(name) AGAINST('%s') AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
table.concat(taglist, ","),
#taglist,
SQLConn:escape(namefilter),
offset
)
elseif #taglist > 0 and not namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s id IN (SELECT objid FROM objtag WHERE tagid IN (%s) GROUP BY objid HAVING COUNT(tagid) >= %s) AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
table.concat(taglist, ","),
#taglist,
offset
)
elseif #taglist == 0 and namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s MATCH(name) AGAINST('%s') AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
SQLConn:escape(namefilter),
offset
)
else
sqlq = string.format(
"SELECT * FROM objects WHERE %s id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
offset
)
end
local cursor = SQLConn:execute(sqlq)
if not cursor then return {} end
local ret = {}
while true do
local row = cursor:fetch({}, "a")
if not row then break end
table.insert(ret, row)
end
return ret
end
local function getobj(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM objects WHERE id = " .. objid .. ";")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id) --for some reason..
ret.owner = tonumber(ret.owner)
ret.published = ret.published:byte(1) ~= 0
ret.approved = ret.approved:byte(1) ~= 0
return ret
end
local hextointandinttohex = {}
for i=0,255 do local s = string.format("%02x", i); hextointandinttohex[s] = i; hextointandinttohex[i] = s; end
local function b256toreadable(src)
local r = {}
src:gsub(".", function(b) table.insert(r, hextointandinttohex[b:byte()]) end)
return table.concat(r)
end
local function readabletob256(src)
if #src % 2 == 1 then error("gotta be even length, man") end
local r = {}
for i=1,#src,2 do
table.insert(r, string.char(hextointandinttohex[src:sub(i, i + 1)]))
end
return table.concat(r)
end
local function isobjhexvalid(objhex)
return #objhex == 32 and not objhex:find("[^0-9a-f]")
end
local function objhideid(objid)
local t = {}
for i=1,16 do
table.insert(t, objid % 256)
objid = objid // 256
end
local c = AES128ECB:encrypt(ObjHideKey, nil, false):final(string.char(table.unpack(t)))
return b256toreadable(c)
end
local function objshowid(objhex)
if not isobjhexvalid(objhex) then return nil end
local c = AES128ECB:decrypt(ObjHideKey, nil, false):final(readabletob256(objhex))
local r = 0
for i=16,1,-1 do
r = r * 256 + c:byte(i)
end
return r
end
local function testobjfilename(fname)
if fname:match"^%s+.*$" or fname:match"^%.+$" or fname:match".*%s+$" or fname:match".*%.+$" then return false end
for k, v in pairs{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} do
if fname == v or fname:match("^" .. v .. "%..+") then
return false
end
end
return not fname:find"[/\\\0-\31%<%>%:%\"%|%?%*]" and #fname <= 56
end
local function urltophysical(p)
if p:sub(1, 6) == "/objd/" then
if isobjhexvalid(p:sub(7, 7 + 31)) and testobjfilename(p:sub(7 + 33)) then
return "objd/" .. p:sub(7, 7 + 31):gsub("..", "%0/") .. p:sub(7 + 32)
end
return nil
else
return p:sub(2)
end
end
-- PERFORMS NO QUOTA CHECKING; THATS DONE IN obje.html.l
local function createfile(objid, fname, fsz)
if not testobjfilename(fname) or fname == ".thumb.jpg" then return false end
if type(fsz) ~= "number" then return false end
local f = io.open(urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname), "wb")
if not f then return false end
if unistd.ftruncate(stdio.fileno(f), fsz) ~= 0 then return false end
f:close()
return true
end
local function updatefile(objid, fname, offset, data)
if not testobjfilename(fname) or fname == ".thumb.jpg" then return false end
local fn = urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname)
if offset + #data > lfs.attributes(fn).size then
return false
end
local f = io.open(fn, "r+b")
if not f then
return false
end
if not f:seek("set", offset) then return false end
if not f:write(data) then return false end
f:close()
return true
end
local function remfilefromobj(objid, fname)
if not testobjfilename(fname) then return false end
if not os.remove(urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname)) then return false end
return true
end
local function genobjthumbnail(objid, fname, maxsize)
if not testobjfilename(fname) then return false end
local objhex = objhideid(objid)
local srcfn = urltophysical("/objd/" .. objhex .. "/" .. fname)
local tempfn = "/tmp/ikibooruthumb" .. b256toreadable(Rand.bytes(16)) .. ".jpg"
local destfn = urltophysical("/objd/" .. objhex .. "/.thumb.jpg")
local ret, killa, exitcode = os.execute("magick convert " .. Escapes.shellescape(srcfn) .. " -background white -strip -interlace Plane -sampling-factor 4:2:0 -flatten -define jpeg:extent=100kb " .. Escapes.shellescape(tempfn))
if not ret or killa ~= "exit" or exitcode ~= 0 then return false end
if LFS.attributes(tempfn).size > maxsize then
os.remove(tempfn)
return false
end
local inn = io.open(tempfn, "rb")
local outt = io.open(destfn, "wb")
if not inn or not outt then
os.remove(tempfn)
return false
end
outt:write(inn:read"*a")
outt:close()
inn:close()
os.remove(tempfn)
return true
end
local function isdisplaynamevalid(str)
return not str:find"[\0-\31]" and #str <= 32
end
local function isemailvalid(str)
return not str:match"^%s*$" and not str:find"[\0-\31]" and #str <= 64
end
local function getuserbyemail(email)
if type(email) ~= "string" then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM users WHERE email = '" .. SQLConn:escape(email) .. "';")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id)
ret.privs = tonumber(ret.privs)
return ret
end
local function getuserbyid(userid)
userid = tonumber(userid)
if not userid then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM users WHERE id = " .. userid .. ";")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id)
ret.privs = tonumber(ret.privs)
return ret
end
local function reguser(email, displayname, privs)
if not isdisplaynamevalid(displayname) then return false, "displayname" end
privs = tonumber(privs)
if not privs then return false, "privs" end
pingdb()
local cursor = SQLConn:execute("INSERT INTO users (displayname, email, createtime, privs) VALUES ('" .. SQLConn:escape(displayname) .. "', '" .. SQLConn:escape(email) .. "', " .. getnow() .. ", " .. privs ..");")
if not cursor then return false, "db" end
return tonumber(SQLConn:getlastautoid())
end
local function userregverify(str)
local start, sep = str:find"^[^%;]+;"
-- Malformity check
if not start then return nil end
local digest = str:sub(1, sep - 1)
local msg = str:sub(sep + 1)
local thetime = msg:match"t%=(%d+)%;"
if not thetime then return nil end
if os.difftime(thetime, os.time()) < 0 then return false end
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return nil
end
if not msg:match"^reg%;" then return nil end
return { em = readabletob256(msg:match"em=([^%;]*)") }
end
local function userregcode(em)
local t = os.date"*t"
t.day = t.day + 7 --7 day expiry
local str = "reg;em="..b256toreadable(em) .. ";t=" .. os.time(t) .. ";"
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function userauth(user)
local t = os.date"*t"
t.day = t.day + 7 --7 day expiry
local str = "id=" .. user.id .. ";t=" .. os.time(t) .. ";"
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function userverify(str)
local start, sep = str:find"^[^%;]+;";
-- Malformity check
if not start then return nil end
local digest = str:sub(1, sep - 1)
local msg = str:sub(sep + 1)
local thetime = msg:match("t%=(%d+)%;")
-- Malformity check
if not thetime then return nil end
-- Expiry check
if os.difftime(thetime, os.time()) < 0 then return false end
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return nil
end
if not msg:match"^id%=" then return nil end
return getuserbyid(msg:match"^id%=(%d+)")
end
local function tagadd(name, category, adultonly)
category = tonumber(category)
if not category then return false end
category = category - 1
pingdb()
return not not SQLConn:execute("INSERT INTO tags (name, category, adultonly) VALUES ('" .. SQLConn:escape(name) .. "', " .. category .. ", " .. (adultonly and 1 or 0) .. ");")
end
local function regnewobj(ownerid, approved, published)
ownerid = tonumber(ownerid)
if not ownerid then return nil end
if type(approved) ~= "boolean" then return nil end
if type(published) ~= "boolean" then return nil end
pingdb()
local cur = SQLConn:execute("INSERT INTO objects (name, owner, approved, published, createtime) VALUES ('', " .. ownerid .. ", " .. (approved and 1 or 0) .. ", " .. (published and 1 or 0) .. ", " .. getnow() .. ");")
if not cur then return nil end
local objid = tonumber(SQLConn:getlastautoid())
local objpath = "objd/"
for objpathpart in objhideid(objid):gmatch("..") do
objpath = objpath .. objpathpart .. "/"
LFS.mkdir(objpath)
end
return objid
end
local function getownedobjs(ownerid)
ownerid = tonumber(ownerid)
if not ownerid then return nil end
pingdb()
local cur = SQLConn:execute("SELECT * FROM objects WHERE owner = " .. ownerid .. " ORDER BY id DESC;")
if not cur then return {} end
local ret = {}
while true do
local row = cur:fetch({}, "a")
if not row then break end
row.id = tonumber(row.id)
row.owner = tonumber(row.owner)
table.insert(ret, row)
end
return ret
end
local function objupdate(objid, newname, newpublished)
if not isdisplaynamevalid(newname) then return false end
objid = tonumber(objid)
if not objid then return false end
if type(newpublished) ~= "boolean" then return false end
pingdb()
if not SQLConn:execute("UPDATE objects SET name = '" .. SQLConn:escape(newname) .. "', published = " .. (newpublished and 1 or 0) .. " WHERE id = " .. objid .. ";") then return false end
return true
end
local function updatetags(tagarr, newname, newcategory, newadultonly, del)
for k,v in pairs(tagarr) do
if type(v) ~= "number" then
return false
end
end
if #tagarr == 0 then return false end
if newcategory and type(newcategory) ~= "number" then
return false
end
pingdb()
if del then
return not not SQLConn:execute("DELETE FROM tags WHERE id IN (" .. table.concat(tagarr, ",") .. ");")
end
local cols = {"adultonly = " .. (newadultonly and 1 or 0)}
if newname then
table.insert(cols, newname and ("name = '" .. SQLConn:escape(newname) .. "'") or "")
end
if newcategory then
newcategory = newcategory - 1
table.insert(cols, newcategory and ("category = " .. newcategory) or "")
end
return not not SQLConn:execute("UPDATE tags SET " .. table.concat(cols, ", ") .. " WHERE id IN (" .. table.concat(tagarr, ",") .. ");")
end
local function csrf(userid)
local str = table.concat({os.time(), tonumber(userid)}, ";")
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function csrfverify(userid, booboo)
if not booboo:find";" then return false end
local digest = booboo:sub(1, booboo:find";" - 1)
local msg = booboo:sub(booboo:find";" + 1)
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return false
end
return tonumber(msg:match";(%d+)$") == userid
end
local function nexttoapproveobj()
pingdb()
local cur = SQLConn:execute("SELECT id FROM objects WHERE published = 1 AND approved = 0 ORDER BY id ASC LIMIT 1;")
if not cur then return nil end
return tonumber(cur:fetch())
end
local function banuser(uid)
if type(uid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_BANNED .. " WHERE id = " .. uid .. ";")
end
local function approveuser(uid)
if type(uid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_APPROVED .. " WHERE id = " .. uid .. " AND privs = " .. USER_PRIVS_UNAPPROVED .. ";")
end
local function delobj(oid, delfiles)
if type(oid) ~= "number" then
return false
end
pingdb()
if not SQLConn:execute("DELETE FROM objects WHERE id = " .. oid .. ";") then
return false
end
if delfiles then
local d = urltophysical("/objd/" .. objhideid(oid))
for f in LFS.dir(d) do
if f ~= "." and f ~= ".." then
os.remove(d .. f)
end
end
end
return true
end
local function approveobj(oid)
if type(oid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE objects SET approved = 1 WHERE id = " .. oid .. ";")
end
local function userinfoupdate(userid, displayname)
if type(userid) ~= "number" then return false end
if not isdisplaynamevalid(displayname) then return false end
pingdb()
return not not SQLConn:execute("UPDATE users SET displayname = '" .. SQLConn:escape(displayname) .. "' WHERE id = " .. userid .. ";")
end
local function getobjtags(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cur = SQLConn:execute("SELECT * FROM tags WHERE id IN (SELECT tagid FROM objtag WHERE objid = " .. objid .. ");")
if not cur then return nil end
local ret = {}
local n = 0
while true do
local t = cur:fetch({}, "*a")
if not t then break end
t.id = tonumber(t.id)
t.category = tonumber(t.category)
ret[t.id] = t
n = n + 1
end
return ret, n
end
local function addtagstoobj(objid, taglist)
if type(objid) ~= "number" then return false end
local temp = {}
for k, v in pairs(taglist) do
if type(v) ~= "number" then return false end
table.insert(temp, "(" .. objid .. "," .. v .. ")")
end
pingdb()
return not not SQLConn:execute("INSERT INTO objtag (objid, tagid) VALUES " .. table.concat(temp, ",") .. ";")
end
local function remtagsfromobj(objid, taglist)
if type(objid) ~= "number" then return false end
local temp = {}
for k, v in pairs(taglist) do
if type(v) ~= "number" then return false end
table.insert(temp, "tagid = " .. v)
end
pingdb()
return not not SQLConn:execute("DELETE FROM objtag WHERE objid = " .. objid .. " AND (" .. table.concat(temp, " OR ") .. ");")
end
local function getmoderators()
pingdb()
local cur = SQLConn:execute("SELECT * from users WHERE privs = " .. USER_PRIVS_MOD .. ";")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.privs = tonumber(t.privs)
table.insert(ret, t)
end
return ret
end
local function setmodsviaemails(emails)
pingdb()
if not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_APPROVED .. " WHERE privs = " .. USER_PRIVS_MOD .. ";") then
return false
end
for k,v in pairs(emails) do
emails[k] = "'" .. SQLConn:escape(v) .. "'"
end
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_MOD .. " WHERE privs < " .. USER_PRIVS_MOD .. " AND email IN (" .. table.concat(emails, ",") .. ");")
end
local function getcomments(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cur = SQLConn:execute("SELECT comment.id AS id, comment.content AS content, comment.authorid AS authorid, user.displayname AS authordisplayname, comment.createtime AS createtime FROM comments comment INNER JOIN users user ON comment.authorid = user.id WHERE comment.objid = " .. objid .. " ORDER BY id DESC;")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.authorid = tonumber(t.authorid)
table.insert(ret, t)
end
return ret
end
local function postcomment(objid, userid, content)
if #content > BigGlobe.MAX_COMMENT_SIZE then
return false
end
pingdb()
return not not SQLConn:execute("INSERT INTO comments (objid, authorid, content, createtime) VALUES (" .. objid .. ", " .. userid .. ", '" .. SQLConn:escape(content) .. "', " .. getnow() .. ")")
end
local function addreport(reporterid, reporteeid, content)
if type(reporterid) ~= "number" then return false end
if type(reporteeid) ~= "number" then return false end
pingdb()
return not not SQLConn:execute("INSERT INTO reports (reporter, reportee, content, createtime, status) VALUES (" .. reporterid .. ", " .. reporteeid .. ", '" .. SQLConn:escape(content) .. "', " .. getnow() .. ", " .. BigGlobe.REPORT_STATUS_OPEN .. ")")
end
local function getreportcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM reports WHERE status = 0;")
if not cur then return nil end
return cur:fetch()
end
local function getreports(status, offset)
if type(status) ~= "number" then return nil end
if type(offset) ~= "number" then offset = 0 end
pingdb()
local cur = SQLConn:execute("SELECT * FROM reports WHERE status = " .. status .. " AND id > " .. offset .. " ORDER BY id ASC LIMIT 50;")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.reporter = tonumber(t.reporter)
t.reportee = tonumber(t.reportee)
t.status = tonumber(t.status)
table.insert(ret, t)
end
return ret
end
local function setreportstatus(rid, newstatus)
if type(rid) ~= "number" then return false end
if type(newstatus) ~= "number" then return false end
pingdb()
return not not SQLConn:execute("UPDATE reports SET status = " .. newstatus .. " WHERE id = " .. rid .. ";")
end
local function getobjcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM objects;")
if not cur then return nil end
return cur:fetch()
end
local function gettagcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM tags;")
if not cur then return nil end
return cur:fetch()
end
return {
USER_PRIVS_BANNED = USER_PRIVS_BANNED,
USER_PRIVS_UNAPPROVED = USER_PRIVS_UNAPPROVED,
USER_PRIVS_APPROVED = USER_PRIVS_APPROVED,
USER_PRIVS_MOD = USER_PRIVS_MOD,
USER_PRIVS_ADMIN = USER_PRIVS_ADMIN,
autocomplete = autocomplete,
searchobjs = searchobjs,
getobj = getobj,
objhideid = objhideid,
objshowid = objshowid,
isobjhexvalid = isobjhexvalid,
urltophysical = urltophysical,
createfile = createfile,
updatefile = updatefile,
remfilefromobj = remfilefromobj,
genobjthumbnail = genobjthumbnail,
isdisplaynamevalid = isdisplaynamevalid,
isemailvalid = isemailvalid,
getuserbyemail = getuserbyemail,
getuserbyid = getuserbyid,
userauth = userauth,
userverify = userverify,
userregcode = userregcode,
userregverify = userregverify,
reguser = reguser,
tagadd = tagadd,
regnewobj = regnewobj,
getownedobjs = getownedobjs,
objupdate = objupdate,
updatetags = updatetags,
b256toreadable = b256toreadable,
readabletob256 = readabletob256,
csrf = csrf,
csrfverify = csrfverify,
nexttoapproveobj = nexttoapproveobj,
banuser = banuser,
approveuser = approveuser,
delobj = delobj,
approveobj = approveobj,
userinfoupdate = userinfoupdate,
getobjtags = getobjtags,
addtagstoobj = addtagstoobj,
remtagsfromobj = remtagsfromobj,
getmoderators = getmoderators,
setmodsviaemails = setmodsviaemails,
getcomments = getcomments,
postcomment = postcomment,
addreport = addreport,
getreportcount = getreportcount,
getreports = getreports,
setreportstatus = setreportstatus,
getobjcount = getobjcount,
gettagcount = gettagcount,
}

35
html.lua Normal file
View File

@ -0,0 +1,35 @@
local SocketUrl = require"socket.url"
local function urlescape(str)
return SocketUrl.escape(str)
end
local function urlunescape(str)
return SocketUrl.unescape(str)
end
local function urlspunescape(str)
return urlunescape(str:gsub("%+", "%%20"))
end
local function htmlescape(str)
return (str:gsub("%&", "&amp;"):gsub("%<", "&lt;"):gsub("%>", "&gt;"):gsub("%\"", "&quot;"):gsub("%'", "&#x27;"))
end
local function shellescape(str)
return "'" .. str:gsub("'", "'\"'\"'") .. "'"
end
local function cssescape(str)
return '"' .. str:gsub('"', '\\"'):gsub('\\', '\\\\') .. '"'
end
return {
urlescape = urlescape,
urlunescape = urlunescape,
urlspunescape = urlspunescape,
htmlescape = htmlescape,
shellescape = shellescape,
patternescape = patternescape,
cssescape = cssescape,
}

84
index.html.l Normal file
View File

@ -0,0 +1,84 @@
{% title = BigGlobe.cfg.sitename .. " - generic object database inspired by imageboards" %}
{% function content() %}
<div style="text-align:center;height:50%;margin-top:25%;">
<div style="max-width:15cm;display:inline-block;">
<h1>{{ Escapes.htmlescape(BigGlobe.cfg.sitename) }}</h1>
<form action="/search" method="GET" id="searchform">
<input type="text" name="n" placeholder="Filter by name..." style="position:relative;width:100%;" />
{% if BigGlobe.cfg.enable18plus then %}
<ul class="over18">
<li>
<input type="radio" name="a" value="-1" id="o18h" onchange="upd(-1)" checked />
<label for="o18h">Hide 18+</label>
</li><li>
<input type="radio" name="a" value="0" id="o18s" onchange="upd(0)" />
<label for="o18s">Show 18+</label>
</li><li>
<input type="radio" name="a" value="1" id="o18o" onchange="upd(1)" />
<label for="o18o">Only 18+</label>
</li>
</ul>
{% end %}
<input type="hidden" id="tparam" name="t" value="" />
<div data-formid="searchform" data-formparaminputid="tparam" style="position:relative;width:100%;margin-top:1em;" class="tagbox">
<p>Filter by tags...</p>
<span style="position:relative;min-width:4px;left:0;" contenteditable></span>
</div>
<div class="autocomplete" style="visibility:hidden;width:100%;"></div>
<input type="submit" value="Search" style="margin-top:1em;" />
<p>Hosting <a href="/search?n=&a=0&t=&hpl">{{DB.getobjcount()}} objects</a> and <a href="#" onclick="showalltags()">{{DB.gettagcount()}} tags</a>.</p>
<div id="alltags"></div>
</form>
</div>
</div>
<script defer src="/static/tagbox.js"></script>
<script defer>
{% if BigGlobe.cfg.enable18plus then %}
function upd(over18) {
document.querySelector(".tagbox").setAttribute("data-over18", +over18 != -1)
window.localStorage.setItem("o18", over18)
}
var activeo18 = "-1"
if(window.localStorage.getItem("o18")) {
activeo18 = window.localStorage.getItem("o18")
}
upd(activeo18)
document.querySelector("ul.over18 input[type=\"radio\"][value=\"" + activeo18 + "\"]").checked = true
{% end %}
function showalltags() {
var ajax = new XMLHttpRequest()
ajax.open("GET", "/autocomp?q=" + (document.querySelector("div.tagbox").getAttribute("data-over18") == "true" ? "&a=" : ""), true)
ajax.onreadystatechange = function() {
if(ajax.readyState == 4 && ajax.status == 200) {
var box = document.getElementById("alltags")
var datags = []
ajax.responseText.split("\n").slice(0, -1).forEach(function(line) {
var newtag = document.createElement("div")
newtag.classList.toggle("tag")
newtag.classList.toggle("tc" + line.split(",")[2])
newtag.setAttribute("data-tagid", line.split(",")[0])
newtag.setAttribute("data-tc", line.split(",")[2])
newtag.innerText = line.split(",")[1]
datags.push(newtag)
})
datags.sort(function(a, b) {return a.getAttribute("data-tc") - b.getAttribute("data-tc")})
datags.forEach(function(x) { box.insertBefore(x, null) })
}