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