/* * 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 "form.h" #include "op.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include struct create { unsigned long uid, category, section, topic; enum auth_role role; time_t creat; char *name, *post; sqlite3 *db; }; static int teardown(struct create *const c) { int ret = 0, error; if (!c) return 0; else if ((error = sqlite3_close(c->db)) != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); ret = -1; } free(c->name); free(c->post); free(c); return ret; } static void free_create(void *const p) { teardown(p); } static int end_post(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; struct create *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 end; } else if (dynstr_append(&d, "/view/%lu/%lu/%jd", c->category, c->section, c->topic)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", d.str)) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); goto end; } free_create(c); ret = 0; end: dynstr_free(&d); return ret; } static int create_post(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, error; struct form *f = NULL; char *spost = NULL; struct dynstr d; const char *post; struct create *const c = args; const time_t t = time(NULL); dynstr_init(&d); if (c->role < AUTH_ROLE_USER) { ret = form_unauthorized("User rights or higher required", r); goto end; } else if (t == (time_t)-1) { fprintf(stderr, "%s: time(2): %s\n", __func__, strerror(errno)); goto end; } else if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error) < 0) fprintf(stderr, "%s: form_alloc failed\n", __func__); goto end; } else if (!(post = form_value(f, "post"))) { ret = form_badreq("Missing post", r); goto end; } else if (!(spost = sanitize(post))) { fprintf(stderr, "%s: sanitize failed\n", __func__); goto end; } else if (dynstr_append(&d, "INSERT INTO posts(text, creat, uid, topid) " "VALUES ('%s', '%jd', %lu, %lu)", spost, (intmax_t)t, c->uid, c->topic)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_create, .end = end_post, .args = c }; if (!op_run(c->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } ret = 0; end: free(spost); form_free(f); dynstr_free(&d); 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) { struct create *const c = args; unsigned t; if (db_uint(c->db, stmt, "topid", &t)) { fprintf(stderr, "%s: db_uint failed\n", __func__); return -1; } c->topic = t; return 0; } static void rollback(void *const args) { struct create *const c = args; if (!c) return; db_rollback(c->db); free_create(c); } static int commit(const struct http_payload *const p, struct http_response *r, void *const user, void *const args) { struct create *const c = args; const struct op_cfg op = { .error = rollback, .end = end_post, .args = c }; if (!op_run(c->db, "COMMIT;", &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); return -1; } return 0; } static int post_inserted(const struct http_payload *const p, struct http_response *r, void *const user, void *const args) { static const char query[] = "SELECT topid FROM posts " "WHERE id = last_insert_rowid();"; struct create *const c = args; const struct op_cfg op = { .error = rollback, .end = commit, .row = topic, .args = c }; if (!op_run(c->db, query, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); return -1; } return 0; } static int topic_inserted(const struct http_payload *const p, struct http_response *r, void *const user, void *const args) { int ret = -1; struct create *const c = args; struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "INSERT INTO posts(text, creat, uid, topid) " "VALUES ('%s', %jd, %lu, last_insert_rowid());", c->post, c->creat, c->uid)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .end = post_inserted, .error = rollback, .args = c }; if (!op_run(c->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 begin_tr(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; struct create *const c = args; struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "INSERT INTO topics(title, creat, secid, uid) " "VALUES ('%s', %jd, %lu, %lu);", c->name, c->creat, c->section, c->uid)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .end = topic_inserted, .error = rollback, .args = c }; if (!op_run(c->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 create_topic(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, error; char *sname = NULL, *spost = NULL; struct form *f = NULL; struct create *const c = args; const char *name, *post; const time_t t = time(NULL); if (c->role < AUTH_ROLE_USER) { ret = form_unauthorized("User rights or higher required", r); goto end; } else if (t == (time_t)-1) { fprintf(stderr, "%s: time(2): %s\n", __func__, strerror(errno)); goto end; } else if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error) < 0) fprintf(stderr, "%s: form_alloc failed\n", __func__); else ret = form_badreq("Missing topic name and/or post", r); goto end; } else if (!(name = form_value(f, "name"))) { ret = form_badreq("Missing topic name", r); goto end; } else if (!(post = form_value(f, "post"))) { ret = form_badreq("Missing post", r); goto end; } else if (!(sname = sanitize(name)) || !(spost = sanitize(post))) { fprintf(stderr, "%s: sanitize failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_create, .end = begin_tr, .args = c }; if (!op_run(c->db, "BEGIN TRANSACTION;", &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } c->name = sname; c->post = spost; c->creat = t; ret = 0; end: if (ret) { free(spost); free(sname); } form_free(f); return ret; } static int end_section(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; struct create *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 end; } else if (dynstr_append(&d, "/view/%lu", c->category)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", d.str)) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); goto end; } free_create(c); ret = 0; end: dynstr_free(&d); return ret; } static int create_section(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, error; struct dynstr d; char *sname = NULL, *sdesc = NULL; struct form *f = NULL; struct create *const c = args; const char *name, *description; dynstr_init(&d); if (c->role < AUTH_ROLE_MOD) { ret = form_unauthorized("Moderator rights required", r); goto end; } else if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error < 0)) fprintf(stderr, "%s: form_alloc failed\n", __func__); else ret = form_badreq("Missing section name and/or title", r); goto end; } else if (!(name = form_value(f, "name"))) { ret = form_badreq("Missing section name", r); goto end; } else if (!(description = form_value(f, "description"))) { ret = form_badreq("Missing section description", r); goto end; } else if (!(sname = sanitize(name)) || !(sdesc = sanitize(description))) { fprintf(stderr, "%s: sanitize failed\n", __func__); goto end; } else if (dynstr_append(&d, "INSERT INTO sections(name, description, catid)" "VALUES('%s', '%s', %lu);", sname, sdesc, c->category)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_create, .end = end_section, .args = c }; if (!op_run(c->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } ret = 0; end: free(sdesc); free(sname); form_free(f); 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) { int ret = -1; struct create *const c = args; const int error = sqlite3_close(c->db); c->db = NULL; if (error != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); goto end; } *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", "/")) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); goto end; } ret = 0; end: free_create(c); return ret; } static int create_category(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, error; char *sname = NULL; struct create *const c = args; struct form *f = NULL; struct dynstr d; const char *name; dynstr_init(&d); if (c->role < AUTH_ROLE_MOD) { ret = form_unauthorized("Moderator rights or higher required", r); goto end; } else if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error) < 0) fprintf(stderr, "%s: form_alloc failed\n", __func__); else ret = form_badreq("Missing category name", r); goto end; } else if (!(name = form_value(f, "name"))) { ret = form_badreq("Expected category name", r); goto end; } else if (!(sname = sanitize(name))) { fprintf(stderr, "%s: sanitize failed\n", __func__); goto end; } else if (dynstr_append(&d, "INSERT INTO categories(name) VALUES" "('%s');", sname)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_create, .end = end_category, .args = c }; if (!op_run(c->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } ret = 0; end: free(sname); dynstr_free(&d); form_free(f); return ret; } static int set_path(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = 0; struct create *const c = args; const char *tree = p->resource + strlen("/create/"); if (!*tree) r->step.payload = create_category; else if (getul(&tree, &c->category)) ret = form_badreq("Invalid category", r); else if (!*tree) r->step.payload = create_section; else if (getul(&tree, &c->section)) ret = form_badreq("Invalid section", r); else if (!*tree) r->step.payload = create_topic; else if (getul(&tree, &c->topic)) ret = form_badreq("Invalid topic", r); else r->step.payload = create_post; return ret; } 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; struct create *c = NULL; if (!u) { ret = form_unauthorized("Authentication required", r); goto failure; } else if (u->role <= AUTH_ROLE_BANNED) { ret = form_unauthorized("Banned account", r); goto failure; } else if (!(c = malloc(sizeof *c))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto failure; } *c = (const struct create) { .uid = u->id, .role = u->role, .db = db }; *r = (const struct http_response) { .step.payload = set_path, .free = free_create, .args = c }; return 0; failure: if ((error = sqlite3_close(db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(c); return ret; } int ep_create(const struct http_payload *const p, struct http_response *const r, void *const user) { int ret = auth_validate(p, r, user, setup); if (ret < 0) fprintf(stderr, "%s: auth_validate failed\n", __func__); else if (ret) ret = form_unauthorized("Authentication required", r); return ret; }