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, }