/* * 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; }