/*
* 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 "defs.h"
#include "db.h"
#include "form.h"
#include "op.h"
#include
#include
#include
#include
#include
#include
#include
struct category
{
sqlite3 *db;
struct op *op;
struct html_node *root, *body, *section_ul, *div;
};
static void free_category(struct category *const c)
{
int error;
if (!c)
return;
else if ((error = sqlite3_close(c->db)) != SQLITE_OK)
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
html_node_free(c->root);
free(c);
}
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 db_section dbs = {0};
struct html_node *div, *ul, *a, *p;
struct dynstr d;
struct category *const c = args;
dynstr_init(&d);
if (db_section(c->db, stmt, &dbs))
{
fprintf(stderr, "%s: db_section failed\n", __func__);
goto end;
}
else if (dynstr_append(&d, "/view/%d/%d", dbs.catid, dbs.id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (!(ul = html_node_add_child(c->div, "ul")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "ul");
goto end;
}
else if (!(div = html_node_add_child(ul, "div")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "ul");
goto end;
}
else if (!(a = html_node_add_child(div, "a")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "a");
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, dbs.name))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (!(p = html_node_add_child(div, "p")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "p");
goto end;
}
else if (html_node_set_value(p, dbs.desc))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
ret = 0;
end:
db_section_free(&dbs);
dynstr_free(&d);
return ret;
}
static int end_section(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct category *const c = args;
op_resume(c->op, r);
return 0;
}
static void rollback(void *const args)
{
struct category *const c = args;
if (!c)
return;
db_rollback(c->db);
free_category(c);
}
static int category( sqlite3_stmt *const stmt,
const struct http_payload *const p, struct http_response *const r,
void *const user, void *const args)
{
int ret = -1;
unsigned long long id;
char *name = NULL;
struct html_node *div, *ul, *a;
struct dynstr d, expr;
struct category *const c = args;
sqlite3 *const db = c->db;
dynstr_init(&d);
dynstr_init(&expr);
if (db_biguint(db, stmt, "id", &id))
{
fprintf(stderr, "%s: failed to get id\n", __func__);
goto end;
}
else if (!(name = db_str(db, stmt, "name")))
{
fprintf(stderr, "%s: failed to get name\n", __func__);
goto end;
}
else if (dynstr_append(&d, "/view/%d", id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if (!(div = html_node_add_child(c->body, "div")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "div");
goto end;
}
else if (!(ul = html_node_add_child(div, "ul")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "ul");
goto end;
}
else if (!(a = html_node_add_child(ul, "a")))
{
fprintf(stderr, "%s: html_node_add_child %s failed\n", __func__, "a");
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, name))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto end;
}
else if (dynstr_append(&expr, "SELECT * FROM sections WHERE catid = %d "
"ORDER BY id ASC;", id))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
const struct op_cfg op =
{
.end = end_section,
.row = section,
.error = rollback,
.args = c
};
if (!op_run(db, expr.str, &op, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto end;
}
c->div = div;
ret = 0;
end:
free(name);
dynstr_free(&expr);
dynstr_free(&d);
return ret;
}
static int end_category(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct category *const c = args;
struct dynstr d;
const int error = sqlite3_close(c->db);
c->db = NULL;
dynstr_init(&d);
if (error != SQLITE_OK)
{
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
goto failure;
}
else if (form_footer(c->body, p->resource))
{
fprintf(stderr, "%s: form_footer failed\n", __func__);
goto failure;
}
else if (dynstr_append(&d, "%s", DOCTYPE_TAG))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto failure;
}
else if (html_serialize(c->root, &d))
{
fprintf(stderr, "%s: html_serialize failed\n", __func__);
goto failure;
}
*r = (const struct http_response)
{
.status = HTTP_STATUS_OK,
.buf.rw = d.str,
.free = free,
.n = d.len
};
free_category(c);
return 0;
failure:
dynstr_free(&d);
return -1;
}
static int commit(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
struct category *const c = args;
const struct op_cfg ocfg =
{
.end = end_category,
.error = rollback,
.args = c
};
if (!op_run(c->db, "COMMIT;", &ocfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
return -1;
}
return 0;
}
static int begin_tr(const struct http_payload *const p,
struct http_response *const r, void *const user, void *const args)
{
static const char query[] = "SELECT * FROM categories ORDER BY id ASC;";
struct category *const c = args;
const struct op_cfg ocfg =
{
.row = category,
.end = commit,
.args = c
};
if (!(c->op = op_run(c->db, query, &ocfg, r)))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
return -1;
}
return 0;
}
static int setup(const struct http_payload *const pl,
struct http_response *const r, void *const user, sqlite3 *const db,
const struct auth_user *const u)
{
int ret = -1, error;
char *userdup = NULL;
const char *const username = u ? u->username : NULL;
struct html_node *root = NULL, *body, *div, *h2, *ul;
struct category *c = NULL;
if (u && u->role <= AUTH_ROLE_BANNED)
{
ret = form_unauthorized("Banned account", r);
goto failure;
}
else 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, username))
{
fprintf(stderr, "%s: form_login failed\n", __func__);
goto failure;
}
else if (u && u->role >= AUTH_ROLE_MOD && form_category(body))
{
fprintf(stderr, "%s: form_category failed\n", __func__);
goto failure;
}
else if (!(div = html_node_add_child(body, "div"))
|| !(h2 = html_node_add_child(div, "h2"))
|| !(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, "Categories"))
{
fprintf(stderr, "%s: html_node_set_value failed\n", __func__);
goto failure;
}
else if (!(c = malloc(sizeof *c)))
{
fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
goto failure;
}
const struct op_cfg ocfg =
{
.error = rollback,
.end = begin_tr,
.args = c
};
if (!op_run(db, "BEGIN TRANSACTION", &ocfg, r))
{
fprintf(stderr, "%s: op_run failed\n", __func__);
goto failure;
}
*c = (const struct category)
{
.section_ul = ul,
.body = body,
.root = root,
.db = db
};
return 0;
failure:
if ((error = sqlite3_close(db)) != SQLITE_OK)
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
free(c);
free(userdup);
html_node_free(root);
return ret;
}
int ep_index(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;
}