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