aboutsummaryrefslogtreecommitdiff
path: root/ep_view.c
diff options
context:
space:
mode:
Diffstat (limited to 'ep_view.c')
-rw-r--r--ep_view.c1180
1 files changed, 1180 insertions, 0 deletions
diff --git a/ep_view.c b/ep_view.c
new file mode 100644
index 0000000..2bc9e47
--- /dev/null
+++ b/ep_view.c
@@ -0,0 +1,1180 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+#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 <libweb/http.h>
+#include <libweb/html.h>
+#include <sqlite3.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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;
+}