thalassa/cms/tcgi_db.cpp
2026-03-19 06:23:52 +05:00

684 lines
21 KiB
C++

#include <stdlib.h> // for rand
#include <inifile/inifile.hpp>
#include "xcaptcha.hpp"
#include "tcgi_sub.hpp"
#include "tcgi_rpl.hpp"
#include "tcgi_ses.hpp"
#include "makeargv.hpp"
#include "roles.hpp"
#include "fnchecks.h"
#include "tcgi_db.hpp"
#ifndef THALASSA_DEFAULT_USERDATA_DIR
#define THALASSA_DEFAULT_USERDATA_DIR "/var/local/thalassa"
#endif
ThalassaCgiDb::ThalassaCgiDb(const Cgi *cgi)
: the_session(0), access_checker(0)
{
inifile = new IniFileParser;
subst = new ThalassaCgiDbSubstitution(this, cgi);
}
ThalassaCgiDb::~ThalassaCgiDb()
{
delete subst;
delete inifile;
if(access_checker)
delete access_checker;
}
bool ThalassaCgiDb::Load(const ScriptVariable &filename)
{
conf_file = filename;
return inifile->Load(filename.c_str());
}
ScriptVariable ThalassaCgiDb::MakeErrorMessage() const
{
ScriptVariable msg("");
int el = inifile->GetLastErrorLine();
if(el != -1)
msg += ScriptNumber(el) + ": ";
else
msg += " ";
msg += inifile->GetLastErrorDescription();
return msg;
}
void ThalassaCgiDb::
GetFormatData(const char *&enc, const char *&tags, const char *&attrs) const
{
static const char defattrs[] = "a=href img=src img=alt";
enc = inifile->GetTextParameter("format", 0, "encoding", 0);
tags = inifile->GetTextParameter("format", 0, "tags", "");
attrs = inifile->GetTextParameter("format", 0, "tag_attributes", defattrs);
}
ScriptVariable ThalassaCgiDb::GetUserdataDirectory() const
{
return inifile->GetTextParameter("general", 0, "userdata_dir",
THALASSA_DEFAULT_USERDATA_DIR);
}
#if 0
ScriptVariable ThalassaCgiDb::GetSessionsDirectory() const
{
ScriptVariable d = GetUserdataDirectory();
if(d != "" && d[d.Length()-1] != '/')
d += '/';
return d + "_sessions";
}
#endif
void ThalassaCgiDb::
GetCaptchaParameters(ScriptVariable &secret, int &time_to_live) const
{
const char *tmp =
inifile->GetTextParameter("captcha", 0, "secret", 0);
if(tmp)
secret = tmp;
else
secret = ScriptNumber(rand()); // so it won't work!
// please note we break it intentionally
// well, the secret MUST be set in the config
// otherwise, the captcha is easily circumvented
time_to_live =
inifile->GetIntegerParameter("captcha", 0, "expire", 300);
}
ScriptVariable ThalassaCgiDb::GetHtmlSnippet(const ScriptVariable &name) const
{
return inifile->GetTextParameter("html", 0, name.c_str(), "");
}
ScriptVariable ThalassaCgiDb::GetContactCategories() const
{
return inifile->GetTextParameter("feedback", 0, "categories", "");
}
ScriptVariable
ThalassaCgiDb::GetContactCatTitle(const ScriptVariable &cg) const
{
const char *tmp = inifile->
GetModifiedTextParameter("feedback", 0, "cattitle", cg.c_str(), 0);
if(tmp)
return tmp;
return ScriptVariable("[==") + cg + "==]";
}
ScriptVariable
ThalassaCgiDb::GetContactRecipient(const ScriptVariable &cg) const
{
const char *c = cg.c_str();
return inifile->GetModifiedTextParameter("feedback", 0, "email", c, "");
}
static bool boolean_value_from_string(const char *str)
{
return str && ScriptVariable(str).Trim().Tolower() == "yes";
}
bool ThalassaCgiDb::GetContactCatIsSel(const ScriptVariable &cg) const
{
const char *tmp = inifile->
GetModifiedTextParameter("feedback", 0, "selected", cg.c_str(), 0);
return boolean_value_from_string(tmp);
}
ScriptVariable ThalassaCgiDb::GetContactEnvelopeFrom() const
{
return inifile->GetTextParameter("feedback", 0, "envelope_from", "");
}
void ThalassaCgiDb::BuildContactFormMessage(const ScriptVariable &receiver,
ScriptVector &command, ScriptVariable &input) const
{
ScriptMacroprocessor sub_sub(subst);
sub_sub.AddMacro(new ScriptMacroConst("receiver", receiver));
ScriptVariable tmp;
tmp = inifile->GetTextParameter("feedback", 0, "send_command", "");
bool ok = make_argv(sub_sub(tmp), command);
if(!ok || command.Length() < 1) {
command.Clear();
input.Invalidate();
return;
}
tmp = inifile->GetTextParameter("feedback", 0, "send_data", "");
tmp.Trim();
if(tmp == "") {
input.Invalidate();
return;
}
input = sub_sub(tmp);
}
void ThalassaCgiDb::BuildServiceEmail(const ScriptVariable &serv_id,
const ScriptVariable &receiver,
const ScriptVector &dict,
ScriptVector &command, ScriptVariable &input) const
{
ScriptVariable subject =
inifile->GetModifiedTextParameter("servicemail", 0, "subject",
serv_id.c_str(), "");
ScriptMacroprocessor sub_sub(subst);
sub_sub.AddMacro(new ScriptMacroConst("receiver", receiver));
int i;
for(i = 0; i < dict.Length(); i+=2)
sub_sub.AddMacro(new ScriptMacroConst(dict[i], dict[i+1]));
subject = sub_sub(subject);
sub_sub.AddMacro(new ScriptMacroConst("subject", subject));
ScriptVariable tmp;
tmp = inifile->GetModifiedTextParameter("servicemail", 0, "send_command",
serv_id.c_str(), "");
bool ok = make_argv(sub_sub(tmp), command);
if(!ok || command.Length() < 1) {
command.Clear();
input.Invalidate();
return;
}
tmp = inifile->GetModifiedTextParameter("servicemail", 0, "header",
serv_id.c_str(), "");
tmp.Trim();
if(tmp == "") {
input.Invalidate();
return;
}
ScriptVariable header = sub_sub(tmp);
tmp = inifile->GetModifiedTextParameter("servicemail", 0, "body",
serv_id.c_str(), "");
tmp.Trim();
if(tmp == "") {
input.Invalidate();
return;
}
input = header + "\n" + sub_sub(tmp);
}
static const char default_error_page_template[] =
"<?xml version=\"1.0\" encoding=\"US-ASCII\" ?>\n"
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n"
" \"http://www.w3.org/TR/xhtml1/DTD/strict.dtd\">\n"
"<html xmlns=\"http://www.w3.org/TR/xhtml1/strict\" >\n"
"<head><title>Thalassa CMS default error page</title></head>\n"
"<body><h1>Thalassa CMS default error page</h1>\n"
"<h2>%errcode% %errmessage%</h2>\n"
"<p>If you see this page it probably means the configuration\n"
"file for Thalassa CMS CGI program is either missing or\n"
"broken. Please check.</p></body></html>\n";
ScriptVariable
ThalassaCgiDb::MakeErrorPage(int code, const ScriptVariable &comment) const
{
ScriptVariable templ =
inifile->GetTextParameter("errorpage", 0, "template",
default_error_page_template);
ScriptNumber code_s(code);
ScriptMacroprocessor sub_sub(subst);
sub_sub.AddMacro(new ScriptMacroConst("errcode", code_s));
sub_sub.AddMacro(new ScriptMacroConst("errmessage", comment));
return sub_sub.Process(templ);
}
long ThalassaCgiDb::GeneralPostLimit() const
{
return inifile->
GetIntegerParameter("general", 0, "post_content_limit", 16);
}
//#include <stdio.h>
int ThalassaCgiDb::FindPath(const ScriptVariable &path, PathData &data) const
{
const char *tmp;
const char *pgn = path.c_str();
bool multipath;
tmp = inifile->GetTextParameter("page", pgn, "template", 0);
if(tmp) {
data.args.Clear();
multipath = false;
} else {
data.args = ScriptWordVector(path, "/");
// note that it's word vector, not token v., so there can't be
// empty tokens AND leading and trailing slashes are removed
if(data.args.Length() < 1)
return path_bad;
pgn = data.args[0].c_str();
tmp = inifile->GetTextParameter("page", pgn, "template", 0);
if(!tmp)
return path_noent;
multipath = true;
}
data.page_id = pgn; // NB: may differ from path!
tmp = inifile->GetTextParameter("page", pgn, "check_fnsafe", 0);
if(tmp) {
ScriptVector tokens;
bool ok = make_argv(subst->Process(tmp, data.args), tokens);
if(!ok) {
//fprintf(stderr, "FAILED make_argv: %s\n", tmp);
return path_invalid;
}
int i;
for(i = 0; i < tokens.Length(); i++)
if(!check_fname_safe(tokens[i].c_str())) {
//fprintf(stderr, "FAILED check: %s\n", tokens[i].c_str());
return path_invalid;
}
//fprintf(stderr, "CHECK_FNSAFE OK\n");
}
tmp = inifile->GetTextParameter("page", pgn, "session_required", 0);
data.session_required = boolean_value_from_string(tmp);
tmp = inifile->GetTextParameter("page", pgn, "embedded", 0);
data.embedded = boolean_value_from_string(tmp);
tmp = inifile->GetTextParameter("page", pgn, "post_allowed", 0);
data.post_allowed = boolean_value_from_string(tmp);
if(data.post_allowed) {
data.post_content_limit =
inifile->GetIntegerParameter("page", pgn, "post_content_limit", -1);
if(data.post_content_limit == -1)
data.post_content_limit = inifile->
GetIntegerParameter("general", 0, "post_content_limit", -1);
data.post_param_limit =
inifile->GetIntegerParameter("page", pgn, "post_param_limit", -1);
if(data.post_param_limit == -1)
data.post_param_limit = inifile->
GetIntegerParameter("general", 0, "post_param_limit", -1);
}
tmp = inifile->GetTextParameter("page", pgn, "reqargs", 0);
//fprintf(stderr, "REQARGS: %s\n", tmp);
if(tmp) {
ScriptWordVector reqargs(tmp);
int i;
for(i = 0; i < reqargs.Length(); i++) {
const char *ra = reqargs[i].c_str();
tmp = inifile->
GetModifiedTextParameter("page", pgn, "reqarg", ra, 0);
if(!tmp)
continue;
subst->SetReqArg(reqargs[i], subst->Process(tmp, data.args));
}
}
if(multipath) {
tmp = inifile->GetTextParameter("page", pgn, "path_predicate", "yes");
//fprintf(stderr, "PATH_PREDICATE: %s\n", tmp);
ScriptVariable r = subst->Process(tmp, data.args);
r.Trim();
r.Tolower();
//fprintf(stderr, "RESULT: %s\n", r.c_str());
if(r == "reject")
return path_noaccess;
if(r != "yes")
return path_notfound;
}
return path_ok;
}
ScriptVariable ThalassaCgiDb::BuildPage(PathData &data) const
{
return BuildResultPage(data, 0, true);
}
ScriptVariable ThalassaCgiDb::BuildResultPage(PathData &data,
const char *msgid, bool is_it_ok) const
{
subst->SetPageData(&data);
if(msgid && *msgid) {
ScriptVariable message;
const char *tmp = inifile->GetTextParameter("message", 0, msgid, 0);
if(tmp)
message = (*subst)(tmp);
else
message = ScriptVariable("[==") + msgid + "==]";
bool requested_action_result =
ScriptVariable(msgid) != "cookie_set";
subst->SetMessage(message, is_it_ok, requested_action_result);
}
const char *tmp;
const char *pgid = data.page_id.c_str();
tmp = inifile->GetTextParameter("page", pgid, "selector", 0);
if(tmp) {
data.selector = subst->Process(tmp, data.args);
data.selector.Trim();
} else {
data.selector = ScriptVariableInv();
}
tmp = inifile->GetTextParameter("page", pgid, "template", 0);
ScriptVariable res = subst->Process(tmp, data.args);
subst->ForgetMessage();
subst->ForgetPageData();
return res;
}
static bool is_alphanumeric(int c)
{
return
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_';
}
bool ThalassaCgiDb::GetPageAction(const PathData &data,
ScriptVector &action) const
{
const char *pgn = data.page_id.c_str();
const char *tmp = inifile->GetTextParameter("page", pgn, "action", 0);
if(!tmp)
return false;
ScriptVariable actstr(tmp);
actstr.Trim();
if(actstr.Length() == 0)
return false;
if(is_alphanumeric(actstr[0])) { // just words, then
action = ScriptWordVector(actstr);
} else { // otherwise, use it as a delimiter for tokens
char dlm[2] = { 0, 0 };
dlm[0] = actstr[0];
actstr.Range(0, 1).Erase();
action = ScriptTokenVector(actstr, dlm, " \t\r\n");
}
if(action.Length() == 0)
return false;
subst->SetPageData(&data);
int i;
for(i = 0; i < action.Length(); i++)
action[i] = subst->Process(action[i], data.args);
subst->ForgetPageData();
return true;
}
ScriptVariable ThalassaCgiDb::GetPageProperty(const PathData &data,
const ScriptVariable &prop_id) const
{
const char *pgn = data.page_id.c_str();
const char *prid = prop_id.c_str();
const char *tmp = data.selector.IsValid() ?
inifile->GetModifiedTextParameter("page", pgn, prid,
data.selector.c_str(), "") :
inifile->GetTextParameter("page", pgn, prid, "");
return subst->Process(tmp, data.args);
//return ScriptVariable("--[") + data.page_id + "|" + prop_id + "]--";
}
ScriptVariable ThalassaCgiDb::MakeNocookiePage() const
{
ScriptVariable templ =
inifile->GetTextParameter("nocookiepage", 0, "template", "");
return subst->Process(templ);
}
ScriptVariable ThalassaCgiDb::MakeRetryCaptchaPage(int captcha_result) const
{
ScriptVariable templ =
inifile->GetTextParameter("retrycaptchapage", 0, "template", "");
const char *modif;
switch(captcha_result) {
case captcha_result_ip_mismatch: modif = "ip_mismatch"; break;
case captcha_result_expired: modif = "expired"; break;
case captcha_result_broken_data: modif = "broken_data"; break;
case captcha_result_wrong: modif = "wrong_answer"; break;
default: modif = "unknown"; break;
}
const char *msg =
inifile->GetModifiedTextParameter("retrycaptchapage", 0, "errmessage",
modif, "captcha problem");
ScriptMacroprocessor sub_sub(subst);
sub_sub.AddMacro(new ScriptMacroConst("errmessage", msg));
return sub_sub.Process(templ);
}
ScriptVariable ThalassaCgiDb::GetCommentDir() const
{
const char *tmp;
tmp = inifile->GetTextParameter("comments", 0, "dir", ".");
return (*subst)(tmp);
}
bool ThalassaCgiDb::GetDiscussionInfo(const ScriptVariable &comment,
DiscussionInfo &result) const
{
ScriptMacroprocessor sub_sub(subst);
sub_sub.AddMacro(new ScriptMacroConst("comment_id", comment));
if(comment.IsInvalid() || comment == "") {
result.comment_id = 0;
} else {
long n;
bool ok = comment.GetLong(n, 10);
if(!ok)
return false;
result.comment_id = n;
}
const char *tmp;
ScriptVariableInv inv;
tmp = inifile->GetTextParameter("comments", 0, "subdir", 0);
if(!tmp)
return false;
result.cmt_tree_dir = GetCommentDir() + "/" + (*subst)(tmp).Trim();
tmp = inifile->GetTextParameter("comments", 0, "page_source", 0);
result.page_source = tmp ? (*subst)(tmp).Trim() : inv;
tmp = inifile->GetTextParameter("comments", 0, "page_html_file", 0);
result.page_html_file = tmp ? (*subst)(tmp).Trim() : inv;
tmp = inifile->GetTextParameter("comments", 0, "page_url", 0);
result.page_url = tmp ? (*subst)(tmp).Trim() : inv;
tmp = inifile->GetTextParameter("comments", 0, "orig_url", 0);
result.orig_url = tmp ? sub_sub(tmp).Trim() : inv;
// NB: the only use of sub_sub!
result.html_start_mark.Invalidate();
result.html_end_mark.Invalidate();
tmp = inifile->GetTextParameter("comments", 0, "page_html_marks", 0);
if(tmp) {
ScriptTokenVector v(tmp, "\n", " \t\r");
int vl = v.Length();
int i = 0;
while(i < vl && v[i] == "")
i++;
if(i < vl)
result.html_start_mark = v[i];
if(i+1 < vl)
result.html_end_mark = v[i+1];
}
tmp = inifile->GetTextParameter("comments", 0, "access", 0);
result.access = tmp ? (*subst)(tmp) : inv;
tmp = inifile->GetTextParameter("comments", 0, "premodq_page_id", 0);
result.premodq_page_id = tmp ? (*subst)(tmp).Trim() : inv;
result.recent_timeout =
inifile->GetIntegerParameter("comments", 0, "recent_timeout", 0);
result.recent_timeout *= 60; /* it is in minutes */
return true;
}
static bool any_whitespace(const char *s)
{
const char *p;
for(p = s; *p; p++)
if(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
return true;
return false;
}
static bool scan_subdir(const ScriptVariable &path, ScriptVector &res)
{
bool have_subdirs = false;
res.Clear();
ReadDir rd(path.c_str());
const char *nm;
while((nm = rd.Next())) {
if(*nm == '.' || *nm == '_' || any_whitespace(nm))
continue;
ScriptVariable fname = path + "/" + nm;
FileStat fs(fname.c_str());
if(!fs.IsDir())
continue;
have_subdirs = true;
ScriptVector sub_subs;
bool sub_has_subs = scan_subdir(fname, sub_subs);
if(!sub_has_subs) {
res.AddItem(nm);
continue;
}
// it's a subdir with subdirs! (the most messy case)
ScriptVariable basename(nm);
basename += "/";
int i;
for(i = 0; i < sub_subs.Length(); i++)
res.AddItem(basename + sub_subs[i]);
}
return have_subdirs;
}
void ThalassaCgiDb::GetCommentTopics(ScriptVector &topics) const
{
ScriptVariable cdr = GetCommentDir();
scan_subdir(cdr, topics);
}
bool ThalassaCgiDb::CommentTopicExists(const ScriptVariable &topic) const
{
ScriptVariable cdr = GetCommentDir();
ScriptVariable fname = cdr + "/" + topic;
FileStat fs(fname.c_str());
if(!fs.Exists() || !fs.IsDir())
return false;
ScriptVector v;
bool has_subdirs = scan_subdir(fname, v);
return !has_subdirs;
}
bool ThalassaCgiDb::MakeCommentPageRegenCmd(ScriptVector &result) const
{
const char *tmp;
tmp = inifile->GetTextParameter("comments", 0, "page_regen_command", 0);
if(!tmp)
return false;
bool ok = make_argv((*subst)(tmp), result);
if(!ok || result.Length() < 1) {
result.Clear();
return false;
}
return true;
}
bool ThalassaCgiDb::CanPost(bool &bypass_premod) const
{
if(!the_session) // this is actually a bug!
return false;
EnsureAccessChecker();
ScriptVector roles;
the_session->GetCurrentRoles(roles);
return access_checker->CanPost(roles, bypass_premod);
}
bool ThalassaCgiDb::CanSeeHidden(const ScriptVariable &comment_owner_id) const
{
if(!the_session) // this is actually a bug!
return false;
EnsureAccessChecker();
ScriptVector roles;
the_session->GetCurrentRoles(roles);
bool own =
the_session->IsLoggedIn() &&
the_session->GetUser() == comment_owner_id;
return access_checker->CanSeeHidden(roles, own);
}
bool ThalassaCgiDb::CanModerate() const
{
if(!the_session) // this is actually a bug!
return false;
EnsureAccessChecker();
ScriptVector roles;
the_session->GetCurrentRoles(roles);
return access_checker->CanModerate(roles);
}
bool ThalassaCgiDb::CanEdit(const ScriptVariable &comment_owner_id,
long long comment_unixdate) const
{
if(!the_session) // this is actually a bug!
return false;
EnsureAccessChecker();
ScriptVector roles;
the_session->GetCurrentRoles(roles);
bool own =
the_session->IsLoggedIn() &&
the_session->GetUser() == comment_owner_id;
return access_checker->CanEdit(roles, own, comment_unixdate);
}
void ThalassaCgiDb::EnsureAccessChecker() const
{
if(access_checker)
return;
const char *rules =
inifile->GetTextParameter("comments", 0, "access", "");
int recent =
inifile->GetIntegerParameter("comments", 0, "recent_timeout", 0);
if(!access_checker)
const_cast<ThalassaCgiDb*>(this)->access_checker =
new AccessChecker();
access_checker->SetRules(rules, recent * 60);
}
void ThalassaCgiDb::SetJustPostedSubst(const ScriptVariable &comment,
bool hidden)
{
subst->SetJustPosted(comment, hidden);
}
void ThalassaCgiDb::CurrentCommentChanged(const ScriptVariable &c) const
{
const char *tmp =
inifile->GetTextParameter("comments", 0, "premodq_page_id", "");
ScriptVariable pg = tmp ? (*subst)(tmp) : ScriptVariableInv();
subst->CurrentCommentChanged(pg, c);
}
void ThalassaCgiDb::SetPreview(const NewCommentData &ncd)
{
subst->SetPreview(ncd);
}