aboutsummaryrefslogtreecommitdiff
path: root/ep_create.c
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-09-22 17:32:44 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2026-02-13 09:57:39 +0100
commit78bf2fe4a5bf37514f6dfd203ef969da0bf40c2e (patch)
tree33f9440b8ee0fa7a3b3ad033616d722d2101bb4d /ep_create.c
parent107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff)
Setup project skeletonHEADmaster
Diffstat (limited to 'ep_create.c')
-rw-r--r--ep_create.c695
1 files changed, 695 insertions, 0 deletions
diff --git a/ep_create.c b/ep_create.c
new file mode 100644
index 0000000..dfe0d6f
--- /dev/null
+++ b/ep_create.c
@@ -0,0 +1,695 @@
+/*
+ * 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 "form.h"
+#include "op.h"
+#include "utils.h"
+#include <dynstr.h>
+#include <libweb/form.h>
+#include <libweb/http.h>
+#include <libweb/html.h>
+#include <sqlite3.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+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;
+}