/*
* nanobbs, a tiny forums software.
* Copyright (C) 2025-2026 Xavier Del Campo Romero
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
#define _POSIX_C_SOURCE 200809L
#include "endpoints.h"
#include "auth.h"
#include "db.h"
#include "defs.h"
#include "form.h"
#include "op.h"
#include "utils.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct view
{
unsigned long category, section, topic, msg, page;
unsigned n_topics;
char *username, *cntq;
struct op *op;
sqlite3 *db;
enum auth_role role;
struct html_node *root, *body, *section_ul, *topic_ul, *topic_div;
int (*end)(const struct http_payload *, struct http_response *,
void *, void *);
union
{
struct category
{
unsigned *secids;
size_t n_secids;
} category;
struct post
{
struct db_user user;
} post;
} u;
};
static const char timefmt[] = "%B %e %Y %H:%M:%S %Z";
static void free_view(struct view *const v)
{
int error;
if (!v)
return;
else if ((error = sqlite3_close(v->db)) != SQLITE_OK)
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
html_node_free(v->root);
free(v->username);
free(v->cntq);
free(v);
}
static int append_section(struct category *const c, const unsigned id)
{
const size_t n = c->n_secids + 1;
unsigned *const secids = realloc(c->secids, n * sizeof *c->secids);
if (!secids)
{
fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
return -1;
}
secids[c->n_secids] = id;
c->secids = secids;
c->n_secids = n;
return 0;
}
static int reply(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1;
struct view *const v = args;
struct dynstr d;
const int error = sqlite3_close(v->db);
v->db = NULL;
dynstr_init(&d);
if (error != SQLITE_OK)
{
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
goto end;
}
else if (dynstr_append(&d, "%s", DOCTYPE_TAG))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (html_serialize(v->root, &d))
{
fprintf(stderr, "%s: html_serialize failed\n", __func__);
goto end;
}
*r = (const struct http_response)
{
.status = HTTP_STATUS_OK,
.buf.rw = d.str,
.n = d.len,
.free = free
};
ret = 0;
end:
if (ret)
dynstr_free(&d);
free(v->u.category.secids);
free_view(v);
return ret;
}
static int post(sqlite3_stmt *const stmt, const struct http_payload *const pl,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1, n;
char *name = NULL, *datetime = NULL, *enctext = NULL;
char id[sizeof "4294967295"];
struct view *const v = args;
sqlite3 *const db = v->db;
struct db_post p = {0};
struct dynstr d;
struct tm tm;
struct html_node *a, *div, *udiv, *tdiv, *pname;
dynstr_init(&d);
/* TODO: push user info */
if (!(div = html_node_add_child(v->body, "div"))
|| !(udiv = html_node_add_child(div, "div"))
|| !(tdiv = html_node_add_child(div, "div"))
|| !(pname = html_node_add_child(tdiv, "p"))
|| !(a = html_node_add_child(tdiv, "a")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (db_post(db, stmt, &p))
{
fprintf(stderr, "%s: db_post failed\n", __func__);
goto end;
}
else if (!(name = db_str(db, stmt, "name")))
{
fprintf(stderr, "%s: db_str failed\n", __func__);
goto end;
}
else if ((n = snprintf(id, sizeof id, "%lu", p.id)) < 0
|| n >= sizeof id)
{
fprintf(stderr, "%s: snprintf(3) returned %d\n", __func__, n);
goto end;
}
else if (dynstr_append(&d, "/view/%lu/%lu/%lu#%lu", v->category,
v->section, v->topic, p.id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (!localtime_r(&p.creat, &tm))
{
fprintf(stderr, "%s: localtime_r(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (!(datetime = astrftime(timefmt, &tm)))
{
fprintf(stderr, "%s: astrftime failed\n", __func__);
goto end;
}
else if (!(enctext = html_encode(p.text)))
{
fprintf(stderr, "%s: html_encode failed\n", __func__);
goto end;
}
else if (html_node_add_attr(a, "href", d.str)
|| html_node_add_attr(div, "id", id))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_node_set_value(a, datetime)
|| html_node_set_value(pname, name))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (html_node_set_value_unescaped(tdiv, enctext))
{
fprintf(stderr, "%s: html_node_set_value_unescaped failed\n", __func__);
goto end;
}
ret = 0;
end:
free(name);
free(enctext);
free(datetime);
db_post_free(&p);
dynstr_free(&d);
return ret;
}
static int end_posts(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
if (form_footer(v->body, p->resource))
{
fprintf(stderr, "%s: form_footer failed\n", __func__);
return -1;
}
*r = (const struct http_response)
{
.step.payload = reply,
.args = v
};
return 0;
}
static void view_error(void *args)
{
free_view(args);
}
static int view_topic(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1;
struct view *const v = args;
struct dynstr d;
struct html_node *const root = html_node_alloc("html"), *body;
dynstr_init(&d);
if (!root)
{
fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
goto end;
}
else if (form_head(root))
{
fprintf(stderr, "%s: form_head failed\n", __func__);
goto end;
}
else if (!(body = html_node_add_child(root, "body")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (form_login(body, v->username))
{
fprintf(stderr, "%s: form_login failed\n", __func__);
goto end;
}
else if (v->username
&& v->role >= AUTH_ROLE_USER
&& form_post(body, v->category, v->section, v->topic))
{
fprintf(stderr, "%s: form_post failed\n", __func__);
goto end;
}
/* TODO: paging */
else if (dynstr_append(&d, "SELECT posts.*, name FROM posts "
"JOIN users ON users.id = posts.uid "
"AND posts.topid = %lu "
"ORDER BY creat ASC;", v->topic))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
const struct op_cfg op =
{
.error = view_error,
.end = end_posts,
.row = post,
.args = v
};
if (!op_run(v->db, d.str, &op, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto end;
}
v->root = root;
v->body = body;
ret = 0;
end:
dynstr_free(&d);
if (ret)
{
html_node_free(root);
free_view(v);
}
return ret;
}
static int end_topics(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
if (form_footer(v->body, p->resource))
{
fprintf(stderr, "%s: form_footer failed\n", __func__);
return -1;
}
*r = (const struct http_response)
{
.step.payload = reply,
.args = v
};
return 0;
}
static void rollback(void *const args)
{
struct view *const v = args;
if (!v)
return;
db_rollback(v->db);
free_view(v);
}
static int commit(const struct http_payload *const p,
struct http_response *const r, struct view *const v)
{
const struct op_cfg cfg =
{
.error = rollback,
.end = v->end,
.args = v,
};
if (!op_run(v->db, "COMMIT;", &cfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
return -1;
}
return 0;
}
static int end_lastmsg(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
const struct view *const v = args;
op_resume(v->op, r);
return 0;
}
static int lastmsg(sqlite3_stmt *const stmt,
const struct http_payload *const p, struct http_response *const r,
void *const user, void *const args)
{
int ret = -1;
char *stime = NULL;
unsigned id;
long long creat;
struct tm tm;
struct dynstr d, url;
struct html_node *span, *a;
struct view *const v = args;
sqlite3 *const db = v->db;
char *const name = db_str(db, stmt, "name");
dynstr_init(&d);
dynstr_init(&url);
if (!name)
{
fprintf(stderr, "%s: db_str failed\n", __func__);
goto end;
}
else if (db_uint(db, stmt, "id", &id))
{
fprintf(stderr, "%s: db_uint failed\n", __func__);
goto end;
}
else if (db_bigint(db, stmt, "creat", &creat))
{
fprintf(stderr, "%s: db_bigint failed\n", __func__);
goto end;
}
else if (!localtime_r(&(time_t){creat}, &tm))
{
fprintf(stderr, "%s: localtime_r(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (!(span = html_node_add_child(v->topic_div, "span"))
|| !(a = html_node_add_child(span, "a")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (!(stime = astrftime(timefmt, &tm)))
{
fprintf(stderr, "%s: astrftime failed\n", __func__);
goto end;
}
else if (dynstr_append(&url, "/view/%u/%u/%u#%u", v->category, v->section,
v->topic, id)
|| dynstr_append(&d, "Last reply by: %s at %s", name, stime))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (html_node_add_attr(a, "href", url.str))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_node_set_value(a, d.str))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
ret = 0;
end:
free(name);
free(stime);
dynstr_free(&d);
dynstr_free(&url);
return ret;
}
static char *gentimestr(const time_t *const creat)
{
char *ret = NULL, *hrtime = NULL, *datetime = NULL;
struct tm tm;
struct dynstr d;
struct html_node *const time = html_node_alloc("time");
dynstr_init(&d);
if (!time)
{
fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
goto end;
}
else if (!(localtime_r(creat, &tm)))
{
fprintf(stderr, "%s: localtime_r(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (!(hrtime = astrftime(timefmt, &tm))
|| !(datetime = astrftime("%Y-%m-%d %H:%M:%S%z", &tm)))
{
fprintf(stderr, "%s: astrftime failed\n", __func__);
goto end;
}
else if (html_node_add_attr(time, "datetime", datetime))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_node_set_value(time, hrtime))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_serialize(time, &d))
{
fprintf(stderr, "%s: html_serialize failed\n", __func__);
goto end;
}
ret = d.str;
end:
if (!ret)
dynstr_free(&d);
html_node_free(time);
free(hrtime);
free(datetime);
return ret;
}
static int topic(sqlite3_stmt *const stmt, const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1;
char *name = NULL, *time = NULL;
struct db_topic t = {0};
struct dynstr url, created, expr;
struct view *const v = args;
sqlite3 *const db = v->db;
struct html_node *li, *dl, *dt, *a, *span;
dynstr_init(&url);
dynstr_init(&created);
dynstr_init(&expr);
if (!(li = html_node_add_child(v->topic_ul, "li"))
|| !(dl = html_node_add_child(li, "dl"))
|| !(dt = html_node_add_child(dl, "dt"))
|| !(v->topic_div = html_node_add_child(dt, "div"))
|| !(a = html_node_add_child(v->topic_div, "a"))
|| !(span = html_node_add_child(v->topic_div, "span")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (db_topic(db, stmt, &t))
{
fprintf(stderr, "%s: db_topic failed\n", __func__);
goto end;
}
else if (!(name = db_str(db, stmt, "name")))
{
fprintf(stderr, "%s: db_str failed\n", __func__);
goto end;
}
else if (!(time = gentimestr(&t.creat)))
{
fprintf(stderr, "%s: gentimestr failed\n", __func__);
goto end;
}
else if (dynstr_append(&url, "/view/%u/%u/%u", v->category, t.secid, t.id)
|| dynstr_append(&created, "Created by %s at %s", name, time))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (html_node_add_attr(a, "href", url.str))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_node_set_value(a, t.title))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (html_node_set_value_unescaped(span, created.str))
{
fprintf(stderr, "%s: html_node_set_value_unescaped failed\n", __func__);
goto end;
}
else if (dynstr_append(&expr, "SELECT posts.id, posts.creat, "
"name FROM posts JOIN users ON posts.topid = %u "
"AND posts.uid = users.id "
"ORDER BY posts.creat DESC LIMIT 1;", t.id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
const struct op_cfg cfg =
{
.end = end_lastmsg,
.row = lastmsg,
.args = v
};
if (!op_run(db, expr.str, &cfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto end;
}
v->section = t.secid;
v->topic = t.id;
ret = 0;
end:
free(name);
free(time);
dynstr_free(&expr);
dynstr_free(&created);
dynstr_free(&url);
db_topic_free(&t);
return ret;
}
static int print_pages(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
fprintf(stderr, "%s: TODO\n", __func__);
v->end = end_topics;
return commit(p, r, v);
}
static int topic_count(sqlite3_stmt *const stmt,
const struct http_payload *const p, struct http_response *const r,
void *const user, void *const args)
{
struct view *const v = args;
unsigned n;
if (db_uint(v->db, stmt, "COUNT(*)", &n))
{
fprintf(stderr, "%s: db_uint failed\n", __func__);
return -1;
}
v->n_topics = n;
return 0;
}
static int count_pages(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
const struct op_cfg cfg =
{
.end = print_pages,
.row = topic_count,
.error = rollback,
.args = v
};
if (!op_run(v->db, v->cntq, &cfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
return -1;
}
return 0;
}
static char *query_topics(const struct view *const v,
const unsigned *const secids, const size_t n_secids)
{
struct dynstr d;
dynstr_init(&d);
if (dynstr_append(&d, "SELECT topics.*, name FROM topics "
"JOIN users ON ("))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
for (size_t i = 0; i < n_secids; i++)
if (dynstr_append(&d, " topics.secid = %u ", secids[i])
|| (i < n_secids - 1 && dynstr_append(&d, "OR")))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
if (dynstr_append(&d, ") AND topics.uid = users.id "
"ORDER BY creat DESC LIMIT %d OFFSET %ju",
PAGE_LIMIT, (uintmax_t)PAGE_LIMIT * v->page))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
return d.str;
failure:
dynstr_free(&d);
return NULL;
}
static char *query_topcnt(const struct view *const v,
const unsigned *const secids, const size_t n_secids)
{
struct dynstr d;
dynstr_init(&d);
if (dynstr_append(&d, "SELECT COUNT(*) FROM topics WHERE"))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
for (size_t i = 0; i < n_secids; i++)
if (dynstr_append(&d, " topics.secid = %u ", secids[i])
|| (i < n_secids - 1 && dynstr_append(&d, "OR")))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
if (dynstr_append(&d, ";"))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
return d.str;
failure:
dynstr_free(&d);
return NULL;
}
static int prepare_topics(struct view *const v,
struct http_response *const r, const unsigned *const secids,
const size_t n_secids)
{
int ret = 1;
char *topq = NULL, *cntq = NULL;
struct html_node *div, *tdiv, *h2;
if (!(div = html_node_add_child(v->body, "div"))
|| !(tdiv = html_node_add_child(v->body, "div"))
|| !(h2 = html_node_add_child(div, "h2"))
|| !(v->topic_ul = html_node_add_child(tdiv, "ul")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (html_node_set_value(h2, "Topics"))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (!(topq = query_topics(v, secids, n_secids)))
{
fprintf(stderr, "%s: query_topics failed\n", __func__);
goto end;
}
else if (!(cntq = query_topcnt(v, secids, n_secids)))
{
fprintf(stderr, "%s: query_topcnt failed\n", __func__);
goto end;
}
const struct op_cfg op =
{
.end = count_pages,
.error = rollback,
.row = topic,
.args = v
};
if (!(v->op = op_run(v->db, topq, &op, r)))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto end;
}
v->cntq = cntq;
ret = 0;
end:
if (ret)
free(cntq);
free(topq);
return ret;
}
static int section_tr(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
const unsigned secids[] = {v->section};
if (prepare_topics(v, r, secids, 1))
{
fprintf(stderr, "%s: prepare_topics failed\n", __func__);
return -1;
}
return 0;
}
static int view_section(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
struct html_node *const root = html_node_alloc("html"), *body;
if (!root)
{
fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
goto failure;
}
else if (form_head(root))
{
fprintf(stderr, "%s: form_head failed\n", __func__);
goto failure;
}
else if (!(body = html_node_add_child(root, "body")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto failure;
}
else if (form_login(body, v->username))
{
fprintf(stderr, "%s: form_login failed\n", __func__);
goto failure;
}
else if (v->username && form_topic(body, v->category, v->section))
{
fprintf(stderr, "%s: form_topic failed\n", __func__);
goto failure;
}
const struct op_cfg ocfg =
{
.end = section_tr,
.error = rollback,
.args = v
};
if (!op_run(v->db, "BEGIN TRANSACTION;", &ocfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto failure;
}
v->root = root;
v->body = body;
return 0;
failure:
html_node_free(root);
return -1;
}
static int end_category(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct view *const v = args;
const struct category *const c = &v->u.category;
if (!c->n_secids)
{
v->end = end_topics;
if (commit(p, r, v))
{
fprintf(stderr, "%s: commit failed\n", __func__);
return -1;
}
}
else if (prepare_topics(v, r, c->secids, c->n_secids))
{
fprintf(stderr, "%s: prepare_topics failed\n", __func__);
return -1;
}
return 0;
}
static int section(sqlite3_stmt *const stmt,
const struct http_payload *const pl, struct http_response *const r,
void *const user, void *const args)
{
int ret = -1;
struct view *const v = args;
struct dynstr d;
struct db_section s = {0};
struct html_node *li, *div, *a, *p;
dynstr_init(&d);
if (!(li = html_node_add_child(v->section_ul, "li"))
|| !(div = html_node_add_child(li, "div"))
|| !(a = html_node_add_child(div, "a"))
|| !(p = html_node_add_child(div, "p")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto end;
}
else if (db_section(v->db, stmt, &s))
{
fprintf(stderr, "%s: db_section failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "/view/%u/%u", s.catid, s.id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (html_node_add_attr(a, "href", d.str))
{
fprintf(stderr, "%s: html_node_add_attr failed\n", __func__);
goto end;
}
else if (html_node_set_value(a, s.name)
|| html_node_set_value(p, s.desc))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (append_section(&v->u.category, s.id))
{
fprintf(stderr, "%s: append_section failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
db_section_free(&s);
return ret;
}
static int category_tr(const struct http_payload *const pl,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1;
struct view *const v = args;
struct dynstr d;
dynstr_init(&d);
if (dynstr_append(&d, "SELECT * FROM sections WHERE "
"sections.catid = %lu ORDER BY sections.id ASC;", v->category))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
const struct op_cfg op =
{
.end = end_category,
.error = rollback,
.row = section,
.args = v
};
if (!op_run(v->db, d.str, &op, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto end;
}
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int view_category(const struct http_payload *const pl,
struct http_response *const r, void *const user, void *const args)
{
int ret = -1;
struct view *const v = args;
struct html_node *root = NULL, *body, *div, *h2;
if (!(root = html_node_alloc("html")))
{
fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
goto failure;
}
else if (form_head(root))
{
fprintf(stderr, "%s: form_head failed\n", __func__);
goto failure;
}
else if (!(body = html_node_add_child(root, "body")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto failure;
}
else if (form_login(body, v->username))
{
fprintf(stderr, "%s: form_login failed\n", __func__);
goto failure;
}
else if (v->username
&& v->role >= AUTH_ROLE_MOD
&& form_section(body, v->category))
{
fprintf(stderr, "%s: form_section failed\n", __func__);
goto failure;
}
else if (!(div = html_node_add_child(body, "div"))
|| !(h2 = html_node_add_child(div, "h2"))
|| !(v->section_ul = html_node_add_child(body, "ul")))
{
fprintf(stderr, "%s: html_node_add_child failed\n", __func__);
goto failure;
}
else if (html_node_set_value(h2, "Sections"))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto failure;
}
const struct op_cfg op =
{
.end = category_tr,
.error = rollback,
.args = v
};
if (!op_run(v->db, "BEGIN TRANSACTION;", &op, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto failure;
}
v->root = root;
v->body = body;
return 0;
failure:
html_node_free(root);
free_view(v);
return ret;
}
static int parse_args(const struct http_payload *const p, struct view *const v)
{
for (size_t i = 0; i < p->n_args; i++)
{
const struct http_arg *const arg = &p->args[i];
const char *const key = arg->key, *const value = arg->value;
if (!strcmp(key, "page") && getul_n(value, &v->page))
{
fprintf(stderr, "%s: invalid page: %s\n", __func__, value);
return -1;
}
else if (!strcmp(key, "msg") && getul_n(value, &v->msg))
{
fprintf(stderr, "%s: invalid msg: %s\n", __func__, value);
return -1;
}
}
return 0;
}
static int setup(const struct http_payload *const p,
struct http_response *const r, void *const user, sqlite3 *const db,
const struct auth_user *const u)
{
int ret = -1, error;
const char *tree = p->resource + strlen("/view/");
struct view *v = NULL;
char *userdup = NULL;
if (!(v = malloc(sizeof *v)))
{
fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
goto failure;
}
*v = (const struct view){.db = db};
*r = (const struct http_response){.args = v};
if (u)
{
if (!(userdup = strdup(u->username)))
{
fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno));
goto failure;
}
else if (u->role <= AUTH_ROLE_BANNED)
{
ret = form_unauthorized("Banned account", r);
goto failure;
}
v->username = userdup;
v->role = u->role;
}
if (parse_args(p, v))
{
ret = form_badreq("Invalid arguments", r);
goto failure;
}
else if (getul(&tree, &v->category))
{
ret = form_badreq("Invalid category", r);
goto failure;
}
else if (!*tree)
r->step.payload = view_category;
else if (getul(&tree, &v->section))
{
ret = form_badreq("Invalid section", r);
goto failure;
}
else if (!*tree)
r->step.payload = view_section;
else if (getul(&tree, &v->topic))
{
ret = form_badreq("Invalid topic", r);
goto failure;
}
else
r->step.payload = view_topic;
return 0;
failure:
if ((error = sqlite3_close(db)) != SQLITE_OK)
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
free(v);
free(userdup);
return ret;
}
int ep_view(const struct http_payload *const p, struct http_response *const r,
void *const user)
{
const int n = auth_validate(p, r, user, setup);
if (n < 0)
fprintf(stderr, "%s: auth_validate failed\n", __func__);
else if (n)
{
const struct cfg *const cfg = user;
sqlite3 *db;
if (db_open(cfg->dir, &db))
{
fprintf(stderr, "%s: db_open failed\n", __func__);
return -1;
}
return setup(p, r, user, db, NULL);
}
return n;
}