diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-09-22 17:32:44 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2026-02-13 09:57:39 +0100 |
| commit | 78bf2fe4a5bf37514f6dfd203ef969da0bf40c2e (patch) | |
| tree | 33f9440b8ee0fa7a3b3ad033616d722d2101bb4d /ep_create.c | |
| parent | 107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff) | |
Diffstat (limited to 'ep_create.c')
| -rw-r--r-- | ep_create.c | 695 |
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; +} |
