misc: correctly proceed to thread-deletion in rb79-delete-post
[rb-79.git] / db-sqlite3.c
blob812f009c7dbbb597d103f5e9a1521a34516bd4de
1 /*
2 * Copyright (c) 2017-2020, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
7 * copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
18 #include <limits.h>
19 #include <stdint.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <time.h>
25 #include <sqlite3.h>
27 #include "macros.h"
28 #include "rb79.h"
30 #define TRY_BIND_T(s, db, i, v) \
31 do { \
32 if ((sqlite3_bind_text((s), \
33 sqlite3_bind_parameter_index((s), (i)), \
34 (v), -1, SQLITE_STATIC)) \
35 != SQLITE_OK) { \
36 ERROR_MESSAGE("sqlite3_bind_text(): cannot bind " \
37 "%s := \"%s\": %s", \
38 (i), (v), \
39 sqlite3_errmsg(db)); \
40 goto done; \
41 } \
42 } while (0)
44 #define TRY_BIND_I(s, db, i, v) \
45 do { \
46 if ((sret = sqlite3_bind_int64((s), \
47 sqlite3_bind_parameter_index((s), \
48 (i)), \
49 (sqlite3_int64) (v))) != \
50 SQLITE_OK) { \
51 ERROR_MESSAGE("sqlite3_bind_int64(): cannot bind " \
52 "%s := %lld: %s", \
53 (i), ((long long int) (v)), \
54 sqlite3_errmsg(db)); \
55 goto done; \
56 } \
57 } while (0)
59 #define TRY_MAKE_STATEMENT_ARRAY(name) \
60 do { \
61 if (!(board_ ## name ## _stmt = \
62 calloc(conf->boards_num, \
63 sizeof *board_ ## name ## _stmt))) \
64 { \
65 PERROR_MESSAGE("calloc"); \
66 goto done; \
67 } \
68 } while (0)
70 #define TRY_PREPARE_FOR_BOARD(j, name) \
71 do { \
72 if (sqlite3_prepare_v2(board_dbs[j], \
73 board_ ## name ## _txt, -1, \
74 &board_ ## name ## _stmt[j], \
75 0) != SQLITE_OK) { \
76 ERROR_MESSAGE("Preparing statement failed: %s", \
77 sqlite3_errmsg(board_dbs[j])); \
78 goto done; \
79 } \
80 } while (0)
82 #define FINALIZE_FOR_BOARD(j, name) \
83 do { \
84 if (board_ ## name ## _stmt) { \
85 sqlite3_finalize(board_ ## name ## _stmt[j]); \
86 board_ ## name ## _stmt[j] = 0; \
87 } \
88 } while (0)
90 #define CLEAN_UP_STATEMENT_ARRAY(name) \
91 do { \
92 free(board_ ## name ## _stmt); \
93 board_ ## name ## _stmt = 0; \
94 } while (0);
96 #define EXFILTRATE_TEXT(s, n, str, len) \
97 do { \
98 char *tmp = (char *) sqlite3_column_text(s, n); \
99 if (tmp) { \
100 str = strdup(tmp); \
101 len = sqlite3_column_bytes(s, n); \
103 else { \
104 str = 0; \
105 len = 0; \
107 } while (0)
109 /* Preparing DBs */
110 static const char *make_comment_table =
111 "create table if not exists comments \n" /* */
112 " (id integer primary key autoincrement,\n" /* */
113 " thread_closed integer default 0, \n" /* */
114 " thread_full integer default 0, \n" /* */
115 " thread_stickied integer default 0, \n" /* */
116 " thread_last_reply integer default 0, \n" /* */
117 " thread_bumpable integer default 1, \n" /* */
118 " in_thread integer default 0, \n" /* */
119 " ip text default '0.0.0.0', \n" /* */
120 " date integer default 0, \n" /* */
121 " name text default 'Anonymous', \n" /* */
122 " tripcode text default '', \n" /* */
123 " email text default '', \n" /* */
124 " comment text default '', \n" /* */
125 " subject text default '', \n" /* */
126 " users_filename text default '', \n" /* */
127 " system_full_path text default '', \n" /* */
128 " system_thumb_path text default '', \n" /* */
129 " file_info text default '' \n" /* */
130 " );"; /* */
131 static const char *make_ban_table =
132 "create table if not exists bans \n" /* */
133 " (id integer primary key autoincrement,\n" /* */
134 " ip_start text default '', \n" /* */
135 " ip_end text default '', \n" /* */
136 " date_start integer default 0, \n" /* */
137 " date_end integer default 0, \n" /* */
138 " reason text default 'No reason given' \n" /* */
139 " );"; /* */
140 static const char *make_cooldown_table =
141 "create table if not exists cooldowns \n" /* */
142 " (ip text primary key, \n" /* */
143 " cooldown_expiry integer default 0 \n" /* */
144 " );"; /* */
145 /* Are you banned globally? */
146 static const char *global_check_ban_txt =
147 "select date_end, reason from bans \n" /* */
148 " where @ip between ip_start and ip_end \n" /* */
149 " and date_end > @now; \n"; /* */
150 static sqlite3_stmt *global_check_ban_stmt;
152 /* Create a global ban */
153 static const char *global_insert_ban_txt =
154 "insert into bans ( ip_start, \n" /* */
155 " ip_end, \n" /* */
156 " date_start, \n" /* */
157 " date_end, \n" /* */
158 " reason) \n" /* */
159 " values (@ip_start, \n" /* */
160 " @ip_end, \n" /* */
161 " @date_start, \n" /* */
162 " @date_end, \n" /* */
163 " @reason); \n"; /* */
164 static sqlite3_stmt *global_insert_ban_stmt;
166 /* Are you banned? */
167 static const char *board_check_ban_txt =
168 "select date_end, reason from bans \n" /* */
169 " where @ip between ip_start and ip_end \n" /* */
170 " and date_end > @now; \n"; /* */
171 static sqlite3_stmt **board_check_ban_stmt;
173 /* When was your last post? */
174 static const char *board_check_cooldown_txt =
175 "select cooldown_expiry from cooldowns \n" /* */
176 "where ip is @ip;";
177 static sqlite3_stmt **board_check_cooldown_stmt;
179 /* That thread, uhh, exists, right? */
180 static const char *board_check_thread_exists_txt =
181 "select thread_closed, thread_full from comments \n" /* */
182 "where id is @thread; \n";
183 static sqlite3_stmt **board_check_thread_exists_stmt;
185 /* How many replies are in this thread? */
186 static const char *board_count_posts_txt =
187 "select count(*) from comments \n" /* */
188 "where coalesce(in_thread, id) is @thread; \n";
189 static sqlite3_stmt **board_count_posts_stmt;
191 /* Delete a thread */
192 static const char *board_delete_thread_txt =
193 "delete from comments where coalesce(in_thread, id) is @thread;";
194 static sqlite3_stmt **board_delete_thread_stmt;
196 /* Delete a post */
197 static const char *board_delete_post_txt =
198 "delete from comments where id is @id;";
199 static sqlite3_stmt **board_delete_post_stmt;
201 /* Get the thread for a post */
202 static const char *board_find_containing_thread_txt =
203 "select coalesce(in_thread, id) from comments \n" /* */
204 "where id is @id order by id; \n";
205 static sqlite3_stmt **board_find_containing_thread_stmt;
207 /* Get the subject of a thread */
208 static const char *board_get_subject_txt =
209 "select subject from comments where id is @thread;";
210 static sqlite3_stmt **board_get_subject_stmt;
212 /* Get the contents of a thread */
213 static const char *board_get_thread_contents_txt =
214 "select id, date, name, subject, email, \n" /* */
215 " tripcode, comment, users_filename, \n" /* */
216 " system_full_path, system_thumb_path, \n" /* */
217 " file_info, ip, thread_closed, \n" /* */
218 " thread_stickied from comments \n" /* */
219 "where coalesce(in_thread, id) is @thread;\n"; /* */
220 static sqlite3_stmt **board_get_thread_contents_stmt;
222 /* Get enough of a thread to write a summary on a board page */
223 static const char *board_get_thread_summary_txt =
224 "select id, date, name, subject, email, \n" /* */
225 " tripcode, comment, users_filename, \n" /* */
226 " system_full_path, system_thumb_path, \n" /* */
227 " file_info, ip, thread_closed, \n" /* */
228 " thread_stickied from comments where \n" /* */
229 " coalesce(in_thread, id) is @thread \n" /* */
230 " and (id in ( \n" /* */
231 " select id from comments where \n" /* */
232 " id is @thread or \n" /* */
233 " in_thread is @thread \n" /* */
234 " order by id desc limit 3 \n" /* */
235 " ) or id is @thread) order by id asc;\n"; /* */
236 static sqlite3_stmt **board_get_thread_summary_stmt;
238 /* Get the contents of a post */
239 static const char *board_get_post_contents_txt =
240 "select id, date, name, subject, email, \n" /* */
241 " tripcode, comment, users_filename, \n" /* */
242 " system_full_path, system_thumb_path, \n" /* */
243 " file_info, ip, in_thread, \n" /* */
244 " thread_closed, thread_stickied \n" /* */
245 " from comments \n" /* */
246 "where id is @post; \n"; /* */
247 static sqlite3_stmt **board_get_post_contents_stmt;
249 /* Create a global ban */
250 static const char *board_insert_ban_txt =
251 "insert into bans ( ip_start, \n" /* */
252 " ip_end, \n" /* */
253 " date_start, \n" /* */
254 " date_end, \n" /* */
255 " reason) \n" /* */
256 " values (@ip_start, \n" /* */
257 " @ip_end, \n" /* */
258 " @date_start, \n" /* */
259 " @date_end, \n" /* */
260 " @reason); \n"; /* */
261 static sqlite3_stmt **board_insert_ban_stmt;
263 /* Make a post/thread/whatever */
264 static const char *board_insert_comment_txt =
265 "insert into comments ( ip, \n" /* */
266 " date, \n" /* */
267 " thread_last_reply,\n" /* */
268 " in_thread, \n" /* */
269 " name, \n" /* */
270 " tripcode, \n" /* */
271 " email, \n" /* */
272 " subject, \n" /* */
273 " comment, \n" /* */
274 " users_filename, \n" /* */
275 " system_full_path, \n" /* */
276 " system_thumb_path \n" /* */
277 " ) \n" /* */
278 " values (@ip, \n" /* */
279 " @date, \n" /* */
280 " @date, \n" /* */
281 " @in_thread, \n" /* */
282 " @name, \n" /* */
283 " @tripcode, \n" /* */
284 " @email, \n" /* */
285 " @subject, \n" /* */
286 " @comment, \n" /* */
287 " @users_filename, \n" /* */
288 " @system_full_path, \n" /* */
289 " @system_thumb_path \n" /* */
290 " ); \n"; /* */
291 static sqlite3_stmt **board_insert_comment_stmt;
293 /* Insert comment part II: adjust the thread */
294 static const char *board_insert_comment_II_txt =
295 "update comments set \n" /* */
296 " thread_last_reply = \n" /* */
297 " (case when @should_bump is 1 and \n" /* */
298 " thread_bumpable is 1 then \n" /* */
299 " @date \n" /* */
300 " else \n" /* */
301 " thread_last_reply \n" /* */
302 " end), \n" /* */
303 " \n" /* */
304 " thread_bumpable = \n" /* */
305 " (case when (select count(*) \n" /* */
306 " from comments \n" /* */
307 " where in_thread is @in_thread) \n" /* */
308 " >= 300 then \n" /* */
309 " 0 \n" /* */
310 " else \n" /* */
311 " thread_bumpable \n" /* */
312 " end), \n" /* */
313 " \n" /* */
314 " thread_full = \n" /* */
315 " (case when (select count(*) \n" /* */
316 " from comments \n" /* */
317 " where in_thread is @in_thread) \n" /* */
318 " >= 500 then \n" /* */
319 " 0 \n" /* */
320 " else \n" /* */
321 " thread_full \n" /* */
322 " end) \n" /* */
323 " where id is @in_thread; \n"; /* */
324 static sqlite3_stmt **board_insert_comment_II_stmt;
326 /* Find all threads on this board */
327 static const char *board_list_threads_txt =
328 "select id from comments where in_thread is NULL\n" /* */
329 "order by thread_stickied desc, \n" /* */
330 " thread_last_reply desc; \n"; /* */
331 static sqlite3_stmt **board_list_threads_stmt;
333 /* Cooldown */
334 static const char *board_set_cooldown_txt =
335 "insert or replace into cooldowns (ip, \n" /* */
336 " cooldown_expiry) \n" /* */
337 "values (@ip, @cooldown_expiry); "; /* */
338 static sqlite3_stmt **board_set_cooldown_stmt;
340 /* Change the sorts of things that moderation needs */
341 static const char *board_update_by_moderation_txt =
342 "update comments set \n" /* */
343 " comment = @comment, \n" /* */
344 " thread_stickied = @thread_stickied, \n" /* */
345 " thread_closed = @thread_closed \n" /* */
346 " where id is @id; \n"; /* */
347 static sqlite3_stmt **board_update_by_moderation_stmt;
349 /* Update the file_info field for a post (after creation) */
350 static const char *board_update_file_info_txt =
351 "update comments set \n" /* */
352 " file_info = @file_info, \n" /* */
353 " system_full_path = @system_full_path, \n" /* */
354 " system_thumb_path = @system_thumb_path \n" /* */
355 " where id is @id; \n"; /* */
356 static sqlite3_stmt **board_update_file_info_stmt;
358 /* Get the last few posts on this board for /recent/ purposes */
359 static const char *board_get_recent_posts_txt =
360 "select id, date, name, subject, email, \n" /* */
361 " tripcode, comment, users_filename, \n" /* */
362 " system_full_path, system_thumb_path, \n" /* */
363 " file_info, ip, in_thread \n" /* */
364 "from comments order by date desc limit 10; \n"; /* */
365 static sqlite3_stmt **board_get_recent_posts_stmt;
367 /* Our connections */
368 static sqlite3 **board_dbs = 0;
369 static size_t num_connected_db = 0;
370 static sqlite3 *global_db = 0;
372 /* Global configuration */
373 const struct configuration *conf;
375 /* Clean the internals of a prepared_post */
376 static void
377 clean_prepared_post(struct prepared_post *p)
379 free(p->name);
380 free(p->subject);
381 free(p->email);
382 free(p->tripcode);
383 free(p->comment);
384 free(p->file_name);
385 free(p->system_full_path);
386 free(p->system_thumb_path);
387 free(p->file_info);
388 free(p->ip);
389 *p = (struct prepared_post) { 0 };
393 * Make sure we can connect to the DBs and that they're in working order
395 * Preconditions:
397 * - setup_dbs() was not invoked more recently than clean_dbs().
399 * Postconditions (success):
401 * - Any other function in this file may be safely called.
404 setup_dbs(const struct configuration *in_conf)
406 int ret = -1;
407 int sret = 0;
408 size_t len = 0;
409 char *path = 0;
410 char *error_message = 0;
412 conf = in_conf;
414 /* Memory for all our board-specific things */
415 if (!(board_dbs = calloc(conf->boards_num, sizeof *board_dbs))) {
416 PERROR_MESSAGE("calloc");
417 goto done;
420 TRY_MAKE_STATEMENT_ARRAY(check_ban);
421 TRY_MAKE_STATEMENT_ARRAY(check_cooldown);
422 TRY_MAKE_STATEMENT_ARRAY(check_thread_exists);
423 TRY_MAKE_STATEMENT_ARRAY(count_posts);
424 TRY_MAKE_STATEMENT_ARRAY(delete_thread);
425 TRY_MAKE_STATEMENT_ARRAY(delete_post);
426 TRY_MAKE_STATEMENT_ARRAY(find_containing_thread);
427 TRY_MAKE_STATEMENT_ARRAY(get_subject);
428 TRY_MAKE_STATEMENT_ARRAY(get_thread_contents);
429 TRY_MAKE_STATEMENT_ARRAY(get_thread_summary);
430 TRY_MAKE_STATEMENT_ARRAY(get_post_contents);
431 TRY_MAKE_STATEMENT_ARRAY(insert_ban);
432 TRY_MAKE_STATEMENT_ARRAY(insert_comment);
433 TRY_MAKE_STATEMENT_ARRAY(insert_comment_II);
434 TRY_MAKE_STATEMENT_ARRAY(list_threads);
435 TRY_MAKE_STATEMENT_ARRAY(set_cooldown);
436 TRY_MAKE_STATEMENT_ARRAY(update_by_moderation);
437 TRY_MAKE_STATEMENT_ARRAY(update_file_info);
438 TRY_MAKE_STATEMENT_ARRAY(get_recent_posts);
440 /* Turn on global connection */
441 len = snprintf(0, 0, "%s/global.db", conf->work_path);
443 if (len + 1 < len) {
444 ERROR_MESSAGE("overflow");
445 goto done;
448 if (!(path = malloc(len + 1))) {
449 PERROR_MESSAGE("malloc");
450 goto done;
453 sprintf(path, "%s/global.db", conf->work_path);
455 if ((sret = sqlite3_open(path, &global_db)) != SQLITE_OK) {
456 ERROR_MESSAGE("Cannot open or create database %s: %s", path,
457 sqlite3_errstr(sret));
458 goto done;
461 /* Set up global table (only bans) */
462 if (sqlite3_exec(global_db, make_ban_table, 0, 0, &error_message) !=
463 SQLITE_OK) {
464 ERROR_MESSAGE("Cannot set up ban table in database %s: %s",
465 path, error_message);
466 goto done;
469 /* Global statments (only ban creation/checking) */
470 if (sqlite3_prepare_v2(global_db, global_check_ban_txt, -1,
471 &global_check_ban_stmt, 0) != SQLITE_OK) {
472 ERROR_MESSAGE("Preparing statement failed: %s", sqlite3_errmsg(
473 global_db));
474 goto done;
477 if (sqlite3_prepare_v2(global_db, global_insert_ban_txt, -1,
478 &global_insert_ban_stmt, 0) != SQLITE_OK) {
479 ERROR_MESSAGE("Preparing statement failed: %s", sqlite3_errmsg(
480 global_db));
481 goto done;
484 /* Board specific stuff */
485 for (size_t j = 0; j < conf->boards_num; ++j) {
486 free(path);
487 path = 0;
488 len = snprintf(0, 0, "%s/board_%s.db", conf->work_path,
489 conf->boards[j].name);
491 if (len + 1 < len) {
492 ERROR_MESSAGE("overflow");
493 goto done;
496 if (!(path = malloc(len + 1))) {
497 PERROR_MESSAGE("malloc");
498 goto done;
501 sprintf(path, "%s/board_%s.db", conf->work_path,
502 conf->boards[j].name);
504 /* Turn on board */
505 if ((sret = sqlite3_open(path, &board_dbs[j])) != SQLITE_OK) {
506 ERROR_MESSAGE("Cannot open or create database %s: %s",
507 path, sqlite3_errstr(sret));
508 goto done;
511 num_connected_db++;
513 /* Set up tables */
514 if (sqlite3_exec(board_dbs[j], make_comment_table, 0, 0,
515 &error_message) != SQLITE_OK) {
516 ERROR_MESSAGE(
517 "Cannot set up comment table in database %s: %s",
518 path,
519 error_message);
520 goto done;
523 if (sqlite3_exec(board_dbs[j], make_ban_table, 0, 0,
524 &error_message) != SQLITE_OK) {
525 ERROR_MESSAGE(
526 "Cannot set up ban table in database %s: %s",
527 path,
528 error_message);
529 goto done;
532 if (sqlite3_exec(board_dbs[j], make_cooldown_table, 0, 0,
533 &error_message) != SQLITE_OK) {
534 ERROR_MESSAGE(
535 "Cannot set up cooldown table in database %s: %s",
536 path,
537 error_message);
538 goto done;
541 free(path);
542 path = 0;
544 /* Set up statements */
545 TRY_PREPARE_FOR_BOARD(j, check_ban);
546 TRY_PREPARE_FOR_BOARD(j, check_cooldown);
547 TRY_PREPARE_FOR_BOARD(j, check_thread_exists);
548 TRY_PREPARE_FOR_BOARD(j, count_posts);
549 TRY_PREPARE_FOR_BOARD(j, delete_thread);
550 TRY_PREPARE_FOR_BOARD(j, delete_post);
551 TRY_PREPARE_FOR_BOARD(j, find_containing_thread);
552 TRY_PREPARE_FOR_BOARD(j, get_subject);
553 TRY_PREPARE_FOR_BOARD(j, get_thread_contents);
554 TRY_PREPARE_FOR_BOARD(j, get_thread_summary);
555 TRY_PREPARE_FOR_BOARD(j, get_post_contents);
556 TRY_PREPARE_FOR_BOARD(j, insert_ban);
557 TRY_PREPARE_FOR_BOARD(j, insert_comment);
558 TRY_PREPARE_FOR_BOARD(j, insert_comment_II);
559 TRY_PREPARE_FOR_BOARD(j, list_threads);
560 TRY_PREPARE_FOR_BOARD(j, set_cooldown);
561 TRY_PREPARE_FOR_BOARD(j, update_by_moderation);
562 TRY_PREPARE_FOR_BOARD(j, update_file_info);
563 TRY_PREPARE_FOR_BOARD(j, get_recent_posts);
566 ret = 0;
567 done:
569 if (error_message) {
570 sqlite3_free(error_message);
573 free(path);
575 return ret;
579 * Construct something suitable for use in <a>
581 * Preconditions:
583 * - setup_dbs() has been invoked more recently than clean_dbs().
585 * - board is a sequence of ASCII characters of length board_len
586 * that represents a board.
588 * - Board directories are located at "/", so that "/".board."/res/"
589 * is where thread pages live.
591 * - post is a sequence of ASCII digits of length post_len.
593 * - out, out_len, and found are not 0.
595 * - Overwriting *out shall not cause a memory leak.
597 * Postconditions:
599 * - If the post doesn't exist, *found = 0.
601 * - Otherwise, *found is 1, and *out is a string like "/a/res/1235"
602 * of length *out_len, which can be used in a <a> element.
605 db_construct_post_link(const char *board, size_t board_len, const char *post,
606 size_t post_len, int *found, char **out, size_t *out_len)
608 int ret = -1;
609 int sret = 0;
610 size_t board_idx = (size_t) -1;
611 size_t in_thread = 0;
612 uintmax_t post_num = 0;
613 sqlite3_stmt *s = 0;
614 sqlite3 *db = 0;
615 size_t len = 0;
616 char *tmp = 0;
618 if (board_len > INT_MAX / 2) {
619 ERROR_MESSAGE("The board name \"%.*s...\" is way too long", 10,
620 board);
621 goto done;
625 * We can't call strtoll(post, 0, 0) because board might
626 * not be 0-terminated, it may point into internal PCRE2
627 * memory for example. It's simpler to recreate base 10
628 * strtoll than to malloc/copy/free a temp buffer.
630 for (size_t j = 0; j < post_len; ++j) {
631 post_num = 10 * post_num + (post[j] - '0');
634 for (size_t j = 0; j < num_connected_db; ++j) {
635 const struct board *b = &conf->boards[j];
637 if (strlen(b->name) == board_len &&
638 !strcmp(board, b->name)) {
639 board_idx = j;
640 break;
644 if (board_idx == (size_t) -1) {
645 ERROR_MESSAGE("Board \"%.*s\" doesn't exist", (int) board_len,
646 board);
647 goto done;
650 s = board_find_containing_thread_stmt[board_idx];
651 db = board_dbs[board_idx];
652 TRY_BIND_I(s, db, "@id", post_num);
653 sret = sqlite3_step(s);
655 switch (sret) {
656 case SQLITE_DONE:
657 *found = 0;
658 ret = 0;
659 goto done;
660 case SQLITE_ROW:
661 in_thread = sqlite3_column_int64(s, 0);
662 break;
663 default:
664 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
665 goto done;
666 break;
669 len = snprintf(0, 0, "/%.*s/res/%zu#post%ju", (int) board_len, board,
670 in_thread, post_num);
672 if (len + 1 < len) {
673 ERROR_MESSAGE("overflow");
674 goto done;
677 if (!(tmp = malloc(len + 1))) {
678 PERROR_MESSAGE("malloc");
679 goto done;
682 sprintf(tmp, "/%.*s/res/%zu#post%ju", (int) board_len, board, in_thread,
683 post_num);
684 *out = tmp;
685 *out_len = len;
686 *found = 1;
687 ret = 0;
688 done:
689 sqlite3_reset(s);
690 sqlite3_clear_bindings(s);
692 return ret;
696 * Ensure that there are not more than the proper number of threads
697 * lying around; report how many pages we need.
699 * Preconditions:
701 * - setup_dbs() has been invoked more recently than clean_dbs().
703 * - board_idx represents a board, AND THE LOCK IS HELD.
705 * - out_thread_ids, out_thread_id_num, and out_num_pages are not 0.
707 * - Overwriting *out_thread_ids shall not cause a memory leak.
709 * Postconditions (success):
711 * - There are num_pages * threads_per_page threads (rows with
712 * in_thread = 0) in the board's database.
714 * - If rows had to be deleted, all relevant reply rows were also
715 * deleted.
717 * - If rows had to be deleted, all files related to those rows
718 * (the thread page, the stored files for replies, etc.) have
719 * been deleted.
722 db_cull_and_report_threads(size_t board_idx, uintmax_t **out_thread_ids,
723 size_t *out_thread_ids_num, size_t *out_num_pages)
725 uintmax_t *to_delete = 0;
726 size_t to_delete_num = 0;
727 size_t to_delete_sz = 0;
728 uintmax_t total_threads_seen = 0;
729 uintmax_t threads_to_keep = 0;
730 int ret = -1;
731 int sret = 0;
732 sqlite3_stmt *s = board_list_threads_stmt[board_idx];
733 sqlite3 *db = board_dbs[board_idx];
734 uint_fast8_t exhausted = 0;
735 void *newmem = 0;
736 uintmax_t *thread_ids = 0;
737 const struct board *b = &conf->boards[board_idx];
739 threads_to_keep = b->num_pages * b->threads_per_page;
741 if (!(thread_ids = calloc(threads_to_keep, sizeof *thread_ids))) {
742 PERROR_MESSAGE("calloc");
743 goto done;
746 if (!(to_delete = malloc(sizeof *to_delete))) {
747 PERROR_MESSAGE("malloc");
748 goto done;
751 to_delete_sz = 1;
752 to_delete[0] = 0;
754 while (!exhausted) {
755 sret = sqlite3_step(s);
757 switch (sret) {
758 case SQLITE_DONE:
759 exhausted = 1;
760 break;
761 case SQLITE_ROW:
762 total_threads_seen++;
764 if (total_threads_seen > threads_to_keep) {
765 to_delete[to_delete_num] = sqlite3_column_int64(
766 s, 0);
768 if (to_delete_num + 1 >= to_delete_sz) {
769 if (to_delete_sz + 16 < to_delete_sz ||
770 ((to_delete_sz + 16) *
771 sizeof *to_delete) /
772 (to_delete_sz +
773 16) !=
774 sizeof *to_delete) {
775 ERROR_MESSAGE("overflow "
776 "(to_delete_sz = %zu)",
777 to_delete_sz);
778 goto done;
781 if (!(newmem = realloc(to_delete,
782 (to_delete_sz +
783 16) *
784 sizeof *to_delete)))
786 PERROR_MESSAGE("relloc");
787 goto done;
790 to_delete = newmem;
791 to_delete_sz += 16;
794 to_delete_num++;
795 } else {
796 thread_ids[total_threads_seen - 1] =
797 sqlite3_column_int64(s, 0);
800 break;
801 default:
802 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
803 goto done;
807 for (size_t j = 0; j < to_delete_num; ++j) {
808 db_remove_thread_and_files(board_idx, to_delete[j]);
811 *out_thread_ids = thread_ids;
812 *out_thread_ids_num = (total_threads_seen > threads_to_keep) ?
813 threads_to_keep : total_threads_seen;
814 *out_num_pages = 0;
816 if (*out_thread_ids_num) {
817 *out_num_pages = 1 + ((*out_thread_ids_num - 1) /
818 b->threads_per_page);
821 ret = 0;
822 done:
823 free(to_delete);
824 to_delete = 0;
825 sqlite3_reset(s);
826 sqlite3_clear_bindings(s);
828 return ret;
832 * Check whether a specific type of ban is active.
834 * Preconditions:
836 * - setup_dbs() has been invoked more recently than clean_dbs().
838 * - s is one of global_check_ban_stmt or a board_check_ban_stmt[j].
840 * - db corresponds to s.
842 * - ip is a string like "127.0.0.1"
844 * - out_is_banned, out_ban_until, out_ban_reason are not 0.
846 * - Overwriting *out_ban_until and *out_ban_reason shall not cause
847 * a memory leak.
849 * Postconditions (success):
851 * - *out_is_banned represents whether s returned a row for ip and
852 * row.
854 * - If *out_banned != 0, then *out_ban_until and *out_ban_reason
855 * are informative text strings (*out_ban_until is something
856 * like "2020-01-01T12:34:56" and *out_ban_reason is something
857 * like "having wrong opinions"). They are not 0.
859 static int
860 check_ban_h(sqlite3_stmt *s, sqlite3 * db, const char *ip, time_t now,
861 int *out_is_banned, char **out_ban_until, char **out_ban_reason)
863 int ret = -1;
864 int sret = 0;
865 size_t dummy_len = 0;
867 UNUSED(dummy_len);
868 TRY_BIND_T(s, db, "@ip", ip);
869 TRY_BIND_I(s, db, "@now", now);
870 sret = sqlite3_step(s);
872 switch (sret) {
873 case SQLITE_DONE:
875 /* No global ban */
876 break;
877 case SQLITE_ROW:
878 *out_is_banned = 1;
879 EXFILTRATE_TEXT(s, 1, *out_ban_reason, dummy_len);
881 if (!(*out_ban_until = util_iso8601_from_time_t(
882 (time_t) sqlite3_column_int64(s, 0)))) {
883 goto done;
886 ret = 0;
887 goto done;
888 default:
889 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
890 goto done;
893 ret = 0;
894 done:
895 sqlite3_reset(s);
896 sqlite3_clear_bindings(s);
898 return ret;
902 * Check whether any ban is active.
904 * Preconditions:
906 * - setup_dbs() has been invoked more recently than clean_dbs().
908 * - ip is a string like "127.0.0.1".
910 * - out_is_banned, out_ban_until, out_ban_reason are not 0.
912 * - Overwriting *out_ban_until and *out_ban_reason shall not cause
913 * a memory leak.
915 * - board_idx corresponds to a board.
917 * Postconditions (success):
919 * - *out_is_banned represents whether a row was found in the bans
920 * db, either globally or for board_idx, matching ip and now.
922 * - If *out_banned != 0, then *out_ban_until and *out_ban_reason
923 * are informative text strings (*out_ban_until is something
924 * like "2020-01-01T12:34:56" and *out_ban_reason is something
925 * like "having wrong opinions"). They are not 0.
928 db_check_bans(const char *ip, size_t board_idx, time_t now, int *out_is_banned,
929 char **out_ban_until, char **out_ban_reason)
931 int ret = -1;
933 /* First check global bans */
934 if (check_ban_h(global_check_ban_stmt, global_db, ip, now,
935 out_is_banned, out_ban_until, out_ban_reason) < 0) {
936 goto done;
939 if (*out_is_banned) {
940 ret = 0;
941 goto done;
944 /* Now board-specific */
945 if (check_ban_h(board_check_ban_stmt[board_idx], board_dbs[board_idx],
946 ip, now, out_is_banned, out_ban_until, out_ban_reason) <
947 0) {
948 goto done;
951 ret = 0;
952 done:
954 return ret;
958 * Check whether a cooldown is active.
960 * Preconditions:
962 * - setup_dbs() has been invoked more recently than clean_dbs().
964 * - ip is a string like "127.0.0.1".
966 * - out_is_cooled, out_cooldown_length are not 0.
968 * - Overwriting *out_cooldown_length shall not cause a memory
969 * leak.
971 * - board_idx corresponds to a board.
973 * Postconditions (success):
975 * - *out_is_cooled represents whether a row was found in the
976 * cooldowns table corresponding to board_idx.
978 * - If *out_is_cooled != 0, then *out_cooldown_length is a string
979 * like "20 seconds", corresponding to the cooldown row.
982 db_check_cooldowns(const char *ip, size_t board_idx, time_t now,
983 int *out_is_cooled, char **out_cooldown_length)
985 int ret = -1;
986 int sret = 0;
987 time_t expiry = 0;
988 long diff = 0;
989 size_t len = 0;
990 sqlite3_stmt *s = board_check_cooldown_stmt[board_idx];
991 sqlite3 *db = board_dbs[board_idx];
993 TRY_BIND_T(s, db, "@ip", ip);
994 sret = sqlite3_step(s);
996 switch (sret) {
997 case SQLITE_DONE:
998 *out_is_cooled = 0;
999 break;
1000 case SQLITE_ROW:
1001 expiry = (time_t) sqlite3_column_int64(s, 0);
1002 diff = (expiry > now) ? expiry - now : -1;
1004 if (diff > 0) {
1005 *out_is_cooled = 1;
1006 len = snprintf(0, 0, "%ld seconds", diff);
1008 if (len + 1 < len) {
1009 ERROR_MESSAGE("overflow");
1010 goto done;
1013 if (!(*out_cooldown_length = malloc(len + 1))) {
1014 PERROR_MESSAGE("malloc");
1015 goto done;
1018 sprintf(*out_cooldown_length, "%ld seconds", diff);
1021 ret = 0;
1022 goto done;
1023 default:
1024 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1025 goto done;
1028 ret = 0;
1029 done:
1030 sqlite3_reset(s);
1031 sqlite3_clear_bindings(s);
1033 return ret;
1037 * Check whether a thread exists, is full, is closed.
1039 * Preconditions:
1041 * - board_idx corresponds to a board.
1043 * - thread_dne, thread_closed, thread_full are not 0.
1045 static int
1046 check_thread(uintmax_t id, size_t board_idx, int *thread_dne,
1047 int *thread_closed, int *thread_full)
1049 int ret = -1;
1050 int sret = 0;
1051 sqlite3 *db = board_dbs[board_idx];
1052 sqlite3_stmt *s = board_check_thread_exists_stmt[board_idx];
1054 TRY_BIND_I(s, db, "@thread", id);
1055 sret = sqlite3_step(s);
1057 switch (sret) {
1058 case SQLITE_DONE:
1059 *thread_dne = 1;
1060 ret = 0;
1061 goto done;
1062 case SQLITE_ROW:
1063 *thread_closed = sqlite3_column_int(s, 0);
1064 *thread_full = sqlite3_column_int(s, 1);
1065 ret = 0;
1066 goto done;
1067 default:
1068 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1069 goto done;
1072 ret = 0;
1073 done:
1074 sqlite3_reset(s);
1075 sqlite3_clear_bindings(s);
1077 return ret;
1080 /* Get the subject of a thread.
1082 * Preconditions:
1084 * - setup_dbs() has been invoked more recently than clean_dbs().
1086 * - board_idx represents a board.
1088 * - thread is the id of a thread.
1090 * - out_subject and out_subject_len are not 0.
1092 * - overwriting *out_subject shall not cause a memory leak.
1094 * Postconditions (success):
1096 * - *out_subject is a string of length *out_subject_len, which
1097 * is the subject of the thread given by thread.
1099 * - The memory of *out_subject should be freed by the caller.
1102 db_extract_subject(size_t board_idx, uintmax_t thread, char **out_subject,
1103 size_t *out_subject_len)
1105 int ret = -1;
1106 int sret = 0;
1107 sqlite3 *db = board_dbs[board_idx];
1108 sqlite3_stmt *s = board_get_subject_stmt[board_idx];
1110 TRY_BIND_I(s, db, "@thread", thread);
1111 sret = sqlite3_step(s);
1113 switch (sret) {
1114 case SQLITE_DONE:
1115 break;
1116 case SQLITE_ROW:
1117 EXFILTRATE_TEXT(s, 0, *out_subject, *out_subject_len);
1118 break;
1119 default:
1120 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1121 goto done;
1124 ret = 0;
1125 done:
1126 sqlite3_reset(s);
1127 sqlite3_clear_bindings(s);
1129 return ret;
1133 * Insert a ban, which may be either global or board-specific.
1135 * Preconditions:
1137 * - setup_dbs() has been invoked more recently than clean_dbs().
1139 * - Either global_ban is non-zero, or board_idx represents a board.
1141 * - first_ip and last_ip are "normalized" ip addresses, in the
1142 * sense of the output of util_normalize_ip(), not e.g. RFC 2373.
1144 * - message is a string.
1146 * Postconditions (success):
1148 * - Depending on global_ban and board_idx, a row in the bans table
1149 * of an appropriate database has been created, depending on the
1150 * input parameters in an obvious way.
1153 db_insert_ban(uint_fast8_t global_ban, size_t board_idx, const char *first_ip,
1154 const char *last_ip, const char *message, time_t ban_start, time_t
1155 ban_expiry)
1157 int ret = -1;
1158 int sret = 0;
1159 sqlite3_stmt *s = 0;
1160 sqlite3 *db = 0;
1162 if (global_ban) {
1163 s = global_insert_ban_stmt;
1164 db = global_db;
1165 } else {
1166 s = board_insert_ban_stmt[board_idx];
1167 db = board_dbs[board_idx];
1170 TRY_BIND_T(s, db, "@ip_start", first_ip);
1171 TRY_BIND_T(s, db, "@ip_end", last_ip);
1172 TRY_BIND_I(s, db, "@date_start", ban_start);
1173 TRY_BIND_I(s, db, "@date_end", ban_expiry);
1174 TRY_BIND_T(s, db, "@reason", message);
1175 sret = sqlite3_step(s);
1177 switch (sret) {
1178 case SQLITE_DONE:
1179 break;
1180 default:
1181 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1182 goto done;
1185 ret = 0;
1186 done:
1187 sqlite3_reset(s);
1188 sqlite3_clear_bindings(s);
1190 return ret;
1194 * Insert a post, which may be a reply or a new thread. Some fields
1195 * (file_info) are not filled out - they are updated later, after
1196 * filesystem work has been completed.
1198 * Preconditions:
1200 * - setup_dbs() has been invoked more recently than clean_dbs().
1202 * - ip is a string like "127.0.0.1".
1204 * - f is not 0 if pc contains a file.
1206 * - The prepared_XYZ fields of pc are filled out.
1208 * - If this post is a reply, in_thread is the id of the thread's
1209 * OP.
1211 * - thread_dne, thread_closed, thread_full, post_id are not 0.
1213 * Postconditions (success):
1215 * - If the post couldn't be made because the thread doesn't exist,
1216 * *thread_dne is 1.
1218 * - Otherwise, if the post couldn't be made because the thread
1219 * is closed, *thread_closed = 1.
1221 * - Otherwise, if the post couldn't be made because the thread
1222 * is full, *thread_full = 1.
1224 * - Otherwise, the post was made, and the surrounding thread's
1225 * reply date, fullness, etc. have been updated (no actual HTML
1226 * regeneration, though).
1228 * - Furthermore, *post_id is the number of the inserted post.
1231 db_insert_post(const char *ip, size_t in_thread, int cooldown, struct
1232 post_cmd *pc, int *thread_dne, int *thread_closed,
1233 int *thread_full,
1234 uintmax_t *post_id)
1236 int ret = -1;
1237 int sret = 0;
1238 sqlite3_stmt *s = board_insert_comment_stmt[pc->board_idx];
1239 sqlite3_stmt *s2 = board_insert_comment_II_stmt[pc->board_idx];
1240 sqlite3_stmt *s3 = board_set_cooldown_stmt[pc->board_idx];
1241 sqlite3 *db = board_dbs[pc->board_idx];
1243 TRY_BIND_T(s, db, "@ip", ip);
1244 TRY_BIND_I(s, db, "@date", pc->prepared.now);
1246 if (in_thread) {
1247 if (check_thread(in_thread, pc->board_idx, thread_dne,
1248 thread_closed, thread_full) < 0) {
1249 goto done;
1252 if (*thread_dne ||
1253 *thread_closed ||
1254 *thread_full) {
1255 ret = 0;
1256 goto done;
1259 TRY_BIND_I(s, db, "@in_thread", in_thread);
1260 TRY_BIND_I(s2, db, "@in_thread", in_thread);
1261 TRY_BIND_I(s2, db, "@date", pc->prepared.now);
1262 TRY_BIND_I(s2, db, "@should_bump", (!pc->prepared.email ||
1263 strcmp(pc->prepared.email,
1264 "sage")));
1267 TRY_BIND_T(s, db, "@name", pc->prepared.name);
1268 TRY_BIND_T(s, db, "@tripcode", pc->prepared.tripcode);
1269 TRY_BIND_T(s, db, "@email", pc->prepared.email);
1270 TRY_BIND_T(s, db, "@subject", pc->prepared.subject);
1271 TRY_BIND_T(s, db, "@comment", pc->prepared.comment);
1272 TRY_BIND_T(s, db, "@users_filename", pc->prepared.file_name);
1275 * It's highly probable that these are blank. At the current
1276 * time of writing, db_insert_post() is called before
1277 * install_files(), and the resulting row is fixed up
1278 * afterwards in db_update_file_info(). These are currently
1279 * left in for the hack-ish hooks for writing posts
1280 * programatically.
1282 TRY_BIND_T(s, db, "@system_full_path", pc->prepared.system_full_path);
1283 TRY_BIND_T(s, db, "@system_thumb_path", pc->prepared.system_thumb_path);
1284 sret = sqlite3_step(s);
1286 switch (sret) {
1287 case SQLITE_DONE:
1288 break;
1289 default:
1290 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1291 goto done;
1294 *post_id = sqlite3_last_insert_rowid(db);
1296 if (in_thread) {
1297 sret = sqlite3_step(s2);
1299 switch (sret) {
1300 case SQLITE_DONE:
1301 case SQLITE_ROW:
1302 break;
1303 default:
1304 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1305 goto done;
1309 TRY_BIND_T(s3, db, "@ip", ip);
1310 TRY_BIND_I(s3, db, "@cooldown_expiry", pc->prepared.now + cooldown);
1311 sret = sqlite3_step(s3);
1313 switch (sret) {
1314 case SQLITE_DONE:
1315 case SQLITE_ROW:
1316 break;
1317 default:
1318 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1319 goto done;
1322 ret = 0;
1323 done:
1324 sqlite3_reset(s);
1325 sqlite3_clear_bindings(s);
1326 sqlite3_reset(s2);
1327 sqlite3_clear_bindings(s2);
1328 sqlite3_reset(s3);
1329 sqlite3_clear_bindings(s3);
1331 return ret;
1335 * Check if a post is actually the OP of a thread.
1337 * Preconditions:
1339 * - setup_dbs() has been invoked more recently than clean_dbs().
1341 * - board_idx represents a board.
1343 * - post_id represents a post.
1345 * - out_is_op is not 0.
1347 * Postconditions (success):
1349 * - *out_is_op is either 1 (if the row with id = post_id has
1350 * in_thread NULL), or 0 (otherwise).
1353 db_is_op(size_t board_idx, uintmax_t post_id, uint_fast8_t *out_is_op)
1355 int ret = -1;
1356 int sret = 0;
1357 sqlite3_stmt *s = board_get_post_contents_stmt[board_idx];
1358 sqlite3 *db = board_dbs[board_idx];
1360 TRY_BIND_I(s, db, "@post", post_id);
1361 sret = sqlite3_step(s);
1363 switch (sret) {
1364 case SQLITE_DONE:
1365 *out_is_op = 0;
1366 break;
1367 case SQLITE_ROW:
1368 *out_is_op = !sqlite3_column_int64(s, 12);
1369 break;
1370 default:
1371 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1372 goto done;
1375 ret = 0;
1376 done:
1377 sqlite3_reset(s);
1378 sqlite3_clear_bindings(s);
1380 return ret;
1384 * Perform minor adjustments to a post
1386 * Preconditions:
1388 * - setup_dbs() has been invoked more recently than clean_dbs().
1390 * - board_idx represents a board.
1392 * - post_id represents a post.
1394 * - moderator_comment is either 0 or a string.
1396 * - If change_sticky or change_close are not 0, then post_id
1397 * represents the OP of a thread.
1399 * Postconditions (success):
1401 * - If change_sticky, then the thread_stickied will be adjusted
1402 * to sticky_status.
1404 * - If change_close, then the thread_closed will be adjusted to
1405 * close_status.
1408 db_moderate_post(size_t board_idx, uintmax_t post_id, const
1409 char *moderator_comment, uint_fast8_t change_sticky,
1410 uint_fast8_t sticky_status,
1411 uint_fast8_t change_close, uint_fast8_t close_status)
1413 int ret = -1;
1414 int sret = 0;
1415 sqlite3_stmt *s = board_get_post_contents_stmt[board_idx];
1416 sqlite3_stmt *s2 = board_update_by_moderation_stmt[board_idx];
1417 sqlite3 *db = board_dbs[board_idx];
1418 uint_fast8_t thread_stickied = 0;
1419 uint_fast8_t thread_closed = 0;
1420 char *comment = 0;
1421 size_t comment_len = 0;
1422 char *new_comment = 0;
1423 size_t new_comment_len = 0;
1425 TRY_BIND_I(s, db, "@post", post_id);
1426 sret = sqlite3_step(s);
1428 switch (sret) {
1429 case SQLITE_DONE:
1430 LOG("Board /%s/, post %ju does not exist",
1431 conf->boards[board_idx].name, post_id);
1432 goto done;
1433 case SQLITE_ROW:
1434 EXFILTRATE_TEXT(s, 6, comment, comment_len);
1435 thread_closed = !!sqlite3_column_int64(s, 13);
1436 thread_stickied = !!sqlite3_column_int64(s, 14);
1437 break;
1438 default:
1439 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1440 goto done;
1443 if (!moderator_comment) {
1444 TRY_BIND_T(s2, db, "@comment", comment);
1445 } else if (comment_len) {
1446 new_comment_len = snprintf(0, 0, "%s<br /><br />"
1447 "<span class=\"mod-text\">"
1448 "(%s)</span>", comment,
1449 moderator_comment);
1451 if (new_comment_len + 1 < new_comment_len) {
1452 ERROR_MESSAGE("overflow");
1453 goto done;
1456 if (!(new_comment = malloc(new_comment_len + 1))) {
1457 PERROR_MESSAGE("malloc");
1458 goto done;
1461 sprintf(new_comment, "%s<br /><br />"
1462 "<span class=\"mod-text\">(%s)</span>",
1463 comment,
1464 moderator_comment);
1465 TRY_BIND_T(s2, db, "@comment", new_comment);
1466 } else {
1467 new_comment_len = snprintf(0, 0, "<span class=\"mod-text\">"
1468 "(%s)</span>",
1469 moderator_comment);
1471 if (new_comment_len + 1 < new_comment_len) {
1472 ERROR_MESSAGE("overflow");
1473 goto done;
1476 if (!(new_comment = malloc(new_comment_len + 1))) {
1477 PERROR_MESSAGE("malloc");
1478 goto done;
1481 sprintf(new_comment, "<span class=\"mod-text\">(%s)</span>",
1482 moderator_comment);
1483 TRY_BIND_T(s2, db, "@comment", new_comment);
1486 if (change_sticky) {
1487 TRY_BIND_I(s2, db, "@thread_stickied", sticky_status);
1488 } else {
1489 TRY_BIND_I(s2, db, "@thread_stickied", thread_stickied);
1492 if (change_close) {
1493 TRY_BIND_I(s2, db, "@thread_closed", close_status);
1494 } else {
1495 TRY_BIND_I(s2, db, "@thread_closed", thread_closed);
1498 TRY_BIND_I(s2, db, "@id", post_id);
1499 sret = sqlite3_step(s2);
1501 switch (sret) {
1502 case SQLITE_DONE:
1503 break;
1504 default:
1505 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1506 goto done;
1509 ret = 0;
1510 done:
1511 free(comment);
1512 free(new_comment);
1513 sqlite3_reset(s);
1514 sqlite3_clear_bindings(s);
1516 return ret;
1520 * Delete all files related to a post, remove row from the
1521 * database.
1523 * Preconditions:
1525 * - setup_dbs() has been invoked more recently than clean_dbs().
1527 * - board_idx represents a board, AND THE LOCK IS HELD.
1529 * - post_id represents a post (a row with in_thread != NULL).
1531 * Postconditions (success):
1533 * - wt_remove_files() has been called on the relevant paths.
1535 * - The row for which id is thread_id has been removed
1536 * from the database.
1539 db_remove_post_and_files(size_t board_idx, uintmax_t post_id)
1541 int ret = -1;
1542 int sret = 0;
1543 sqlite3_stmt *s = board_get_post_contents_stmt[board_idx];
1544 sqlite3_stmt *s2 = board_delete_post_stmt[board_idx];
1545 sqlite3 *db = board_dbs[board_idx];
1546 char *system_full_path = 0;
1547 size_t system_full_path_len = 0;
1548 char *system_thumb_path = 0;
1549 size_t system_thumb_path_len = 0;
1551 TRY_BIND_I(s, db, "@post", post_id);
1552 sret = sqlite3_step(s);
1554 switch (sret) {
1555 case SQLITE_DONE:
1556 LOG("Board /%s/, post %ju does not exist",
1557 conf->boards[board_idx].name, post_id);
1558 goto done;
1559 case SQLITE_ROW:
1560 EXFILTRATE_TEXT(s, 8, system_full_path, system_full_path_len);
1561 EXFILTRATE_TEXT(s, 9, system_thumb_path, system_thumb_path_len);
1562 wt_remove_files(system_full_path, system_full_path_len,
1563 system_thumb_path, system_thumb_path_len);
1564 break;
1565 default:
1566 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1567 goto done;
1570 TRY_BIND_I(s2, db, "@id", post_id);
1571 sret = sqlite3_step(s2);
1573 switch (sret) {
1574 case SQLITE_DONE:
1575 break;
1576 default:
1577 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1578 goto done;
1581 ret = 0;
1582 done:
1583 free(system_full_path);
1584 free(system_thumb_path);
1585 sqlite3_reset(s);
1586 sqlite3_reset(s2);
1587 sqlite3_clear_bindings(s);
1588 sqlite3_clear_bindings(s2);
1590 return ret;
1594 * Delete all files related to a thread, remove rows from the
1595 * database.
1597 * Preconditions:
1599 * - setup_dbs() has been invoked more recently than clean_dbs().
1601 * - board_idx represents a board, AND THE LOCK IS HELD.
1603 * - thread_id represents a thread (a row with in_thread = NULL).
1605 * Postconditions (success):
1607 * - For every post in the thread, wt_remove_files() has been
1608 * called on the relevant paths.
1610 * - wt_remove_thread_page() has been called on the relevant thread.
1612 * - Any row for which in_thread is thread_id has been removed
1613 * from the database.
1616 db_remove_thread_and_files(size_t board_idx, uintmax_t thread_id)
1618 int ret = -1;
1619 int sret = 0;
1620 sqlite3_stmt *s = board_get_thread_contents_stmt[board_idx];
1621 sqlite3_stmt *s2 = board_delete_thread_stmt[board_idx];
1622 sqlite3 *db = board_dbs[board_idx];
1623 char *system_full_path = 0;
1624 size_t system_full_path_len = 0;
1625 char *system_thumb_path = 0;
1626 size_t system_thumb_path_len = 0;
1627 char first_try = 1;
1629 TRY_BIND_I(s, db, "@thread", thread_id);
1630 again:
1631 sret = sqlite3_step(s);
1633 switch (sret) {
1634 case SQLITE_DONE:
1636 if (first_try) {
1637 LOG("Board /%s/, post %ju does not exist",
1638 conf->boards[board_idx].name, thread_id);
1639 goto done;
1642 goto nowthread;
1643 case SQLITE_ROW:
1644 EXFILTRATE_TEXT(s, 8, system_full_path, system_full_path_len);
1645 EXFILTRATE_TEXT(s, 9, system_thumb_path, system_thumb_path_len);
1646 wt_remove_files(system_full_path, system_full_path_len,
1647 system_thumb_path, system_thumb_path_len);
1649 /* Clean up */
1650 free(system_full_path);
1651 free(system_thumb_path);
1652 system_full_path = 0;
1653 system_thumb_path = 0;
1654 first_try = 0;
1655 goto again;
1656 default:
1657 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1658 goto done;
1661 nowthread:
1663 if (wt_remove_thread_page(board_idx, thread_id) < 0) {
1664 goto done;
1667 TRY_BIND_I(s2, db, "@thread", thread_id);
1668 sret = sqlite3_step(s2);
1670 switch (sret) {
1671 case SQLITE_DONE:
1672 break;
1673 default:
1674 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1675 goto done;
1678 ret = 0;
1679 done:
1680 sqlite3_reset(s);
1681 sqlite3_reset(s2);
1682 sqlite3_clear_bindings(s);
1683 sqlite3_clear_bindings(s2);
1685 return ret;
1689 * Update the file_info field for a comment (as it isn't known at insert time)
1691 * Preconditions:
1693 * - setup_dbs() has been invoked more recently than clean_dbs().
1695 * - board_idx represents a board.
1697 * - post_id is the id of a row that exists in that board's comments.
1699 * - info is a string of length info_len.
1701 * Postconditions (success):
1703 * - `select file_info from comments where id is @post_id', on the
1704 * correct board, would return info.
1707 db_update_file_info(size_t board_idx, uintmax_t post_id, const char *info,
1708 size_t info_len, const char *system_full_path, size_t
1709 system_full_path_len,
1710 const char *system_thumb_path, size_t system_thumb_path_len)
1712 int ret = -1;
1713 int sret = 0;
1714 sqlite3 *db = board_dbs[board_idx];
1715 sqlite3_stmt *s = board_update_file_info_stmt[board_idx];
1717 /* XXX: use this in TRY_BIND_T */
1718 UNUSED(info_len);
1719 UNUSED(system_full_path_len);
1720 UNUSED(system_thumb_path_len);
1721 TRY_BIND_I(s, db, "@id", post_id);
1722 TRY_BIND_T(s, db, "@file_info", info);
1723 TRY_BIND_T(s, db, "@system_full_path", system_full_path);
1724 TRY_BIND_T(s, db, "@system_thumb_path", system_thumb_path);
1725 sret = sqlite3_step(s);
1727 switch (sret) {
1728 case SQLITE_DONE:
1729 ret = 0;
1730 goto done;
1731 default:
1732 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1733 goto done;
1736 ret = 0;
1737 done:
1738 sqlite3_reset(s);
1739 sqlite3_clear_bindings(s);
1741 return ret;
1745 * The db side of write_thread(): pull all posts from a thread, and
1746 * call pw_function as appropriate (the abstraction is for the sake
1747 * of rb79-view-thread).
1749 * *pw_function had better be one of wt_write_post(),
1751 * Preconditions:
1753 * - setup_dbs() has been invoked more recently than clean_dbs().
1755 * - board_idx represents a board.
1757 * - thread is the id of a thread.
1759 * - f is an open filehandle that can be written to.
1761 * - *pw_function is something like wt_write_post().
1763 * Postconditions (success):
1765 * - The thread has, somehow, been written out to f. In practice,
1766 * this means wt_write_post() has been called on the rows that
1767 * correspond to thread.
1770 db_writeback_posts_in_thread(size_t board_idx, uintmax_t thread, FILE *f,
1771 post_writeback pw_function)
1773 int ret = -1;
1774 int sret = 0;
1775 uint_fast8_t first_post = 1;
1776 struct prepared_post p = { 0 };
1777 sqlite3_stmt *s = board_get_thread_contents_stmt[board_idx];
1778 sqlite3 *db = board_dbs[board_idx];
1780 TRY_BIND_I(s, db, "@thread", thread);
1781 again:
1782 sret = sqlite3_step(s);
1784 switch (sret) {
1785 case SQLITE_DONE:
1786 ret = 0;
1787 goto done;
1788 break;
1789 case SQLITE_ROW:
1790 p = (struct prepared_post) { 0 };
1791 p.id = sqlite3_column_int64(s, 0);
1792 p.now = sqlite3_column_int64(s, 1);
1793 EXFILTRATE_TEXT(s, 2, p.name, p.name_len);
1794 EXFILTRATE_TEXT(s, 3, p.subject, p.subject_len);
1795 EXFILTRATE_TEXT(s, 4, p.email, p.email_len);
1796 EXFILTRATE_TEXT(s, 5, p.tripcode, p.tripcode_len);
1797 EXFILTRATE_TEXT(s, 6, p.comment, p.comment_len);
1798 EXFILTRATE_TEXT(s, 7, p.file_name, p.file_name_len);
1799 EXFILTRATE_TEXT(s, 8, p.system_full_path,
1800 p.system_full_path_len);
1801 EXFILTRATE_TEXT(s, 9, p.system_thumb_path,
1802 p.system_thumb_path_len);
1803 EXFILTRATE_TEXT(s, 10, p.file_info, p.file_info_len);
1804 EXFILTRATE_TEXT(s, 11, p.ip, p.ip_len);
1805 p.thread_closed = !!sqlite3_column_int64(s, 12);
1806 p.thread_stickied = !!sqlite3_column_int64(s, 13);
1808 if ((*pw_function)(&p, f, first_post, 0, 0, 0, 0, 0, 0) < 0) {
1809 goto done;
1812 first_post = 0;
1813 clean_prepared_post(&p);
1814 goto again;
1815 default:
1816 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1817 goto done;
1820 ret = 0;
1821 done:
1822 sqlite3_reset(s);
1823 sqlite3_clear_bindings(s);
1825 return ret;
1829 * The db side of write_recent_page(): pull recent posts from all
1830 * boards, sort, and call pw_function as appropriate.
1832 * *pw_function had better be one of wt_write_recent_post(),
1834 * Preconditions:
1836 * - setup_dbs() has been invoked more recently than clean_dbs().
1838 * - f is an open filehandle that can be written to.
1840 * - *pw_function is something like wt_write_recent_post().
1842 * Postconditions (success):
1844 * - The few most recent posts of the board have, somehow, been
1845 * written out to f. In practice, this means wt_write_recent_post()
1846 * has been called on the rows that correspond to thread.
1849 db_writeback_recent_posts(FILE *f, post_writeback pw_function)
1851 int ret = -1;
1852 int sret = 0;
1853 struct prepared_post p[10];
1854 size_t on_board[10];
1855 uintmax_t in_thread[10];
1856 time_t now = 0;
1857 sqlite3_stmt *s = 0;
1858 sqlite3 *db = 0;
1860 for (size_t k = 0; k < 10; ++k) {
1861 p[k] = (struct prepared_post) { 0 };
1862 on_board[k] = (size_t) -1;
1865 for (size_t j = 0; j < conf->boards_num; ++j) {
1866 s = board_get_recent_posts_stmt[j];
1867 db = board_dbs[j];
1868 again:
1869 sret = sqlite3_step(s);
1871 switch (sret) {
1872 case SQLITE_DONE:
1873 break;
1874 case SQLITE_ROW:
1875 now = sqlite3_column_int64(s, 1);
1877 for (size_t k = 0; k < 10; ++k) {
1878 if (p[k].now >= now) {
1879 continue;
1882 clean_prepared_post(&p[9]);
1884 for (size_t l = 9; l >= k + 1; --l) {
1885 memcpy(&p[l], &p[l - 1], sizeof p[0]);
1886 on_board[l] = on_board[l - 1];
1887 in_thread[l] = in_thread[l - 1];
1890 on_board[k] = j;
1891 p[k].id = sqlite3_column_int64(s, 0);
1892 p[k].now = sqlite3_column_int64(s, 1);
1893 EXFILTRATE_TEXT(s, 2, p[k].name, p[k].name_len);
1894 EXFILTRATE_TEXT(s, 3, p[k].subject,
1895 p[k].subject_len);
1896 EXFILTRATE_TEXT(s, 4, p[k].email,
1897 p[k].email_len);
1898 EXFILTRATE_TEXT(s, 5, p[k].tripcode,
1899 p[k].tripcode_len);
1900 EXFILTRATE_TEXT(s, 6, p[k].comment,
1901 p[k].comment_len);
1902 EXFILTRATE_TEXT(s, 7, p[k].file_name,
1903 p[k].file_name_len);
1904 EXFILTRATE_TEXT(s, 8, p[k].system_full_path,
1905 p[k].system_full_path_len);
1906 EXFILTRATE_TEXT(s, 9, p[k].system_thumb_path,
1907 p[k].system_thumb_path_len);
1908 EXFILTRATE_TEXT(s, 10, p[k].file_info,
1909 p[k].file_info_len);
1910 EXFILTRATE_TEXT(s, 11, p[k].ip, p[k].ip_len);
1911 in_thread[k] = sqlite3_column_int64(s, 12);
1913 if (!in_thread[k]) {
1914 in_thread[k] = p[k].id;
1917 break;
1920 goto again;
1921 default:
1922 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
1923 goto done;
1926 sqlite3_reset(s);
1929 for (size_t k = 0; k < 10; ++k) {
1930 const struct board *b = 0;
1932 if (on_board[k] >= conf->boards_num) {
1933 continue;
1936 b = &conf->boards[on_board[k]];
1938 if ((*pw_function)(&p[k], f, 0, 0, 1, b->name, in_thread[k], 0,
1939 !!k) < 0) {
1940 goto done;
1944 done:
1946 for (size_t k = 0; k < 10; ++k) {
1947 clean_prepared_post(&p[k]);
1950 return ret;
1954 * The db side of write_board(): pull enough posts from a thread
1955 * to create a summary for the board page.
1957 * Preconditions:
1959 * - setup_dbs() has been invoked more recently than clean_dbs().
1961 * - board_idx represents a board.
1963 * - thread_ids is an array of size (at least) thread_ids_num.
1965 * - each element of thread_ids represents a currently-active
1966 * thread, and the list is sorted by bump order (most recent
1967 * first).
1969 * - f is an open filehandle that can be written to.
1971 * Postconditions (success):
1973 * - Each of the thread_ids_num threads represented in thread_ids
1974 * has, somehow, been written out to f. In practice, this means
1975 * write_op_in_board() and write_post_in_board() have been called
1976 * on the rows that correspond to the OP and the last few posts
1977 * of a thread.
1980 db_writeback_thread_summaries(size_t board_idx, uintmax_t *thread_ids, size_t
1981 thread_ids_num, FILE *f)
1983 int ret = -1;
1984 int sret = 0;
1985 uint_fast8_t first_post = 1;
1986 sqlite3_stmt *s = board_count_posts_stmt[board_idx];
1987 sqlite3_stmt *s2 = board_get_thread_summary_stmt[board_idx];
1988 sqlite3 *db = board_dbs[board_idx];
1989 struct prepared_post p = { 0 };
1991 for (size_t j = 0; j < thread_ids_num; ++j) {
1992 uintmax_t total_post_num = 0;
1993 uintmax_t thread_id = thread_ids[j];
1995 first_post = 1;
1996 sqlite3_reset(s);
1997 sqlite3_clear_bindings(s);
1998 TRY_BIND_I(s, db, "@thread", thread_id);
1999 sret = sqlite3_step(s);
2001 switch (sret) {
2002 case SQLITE_DONE:
2003 break;
2004 case SQLITE_ROW:
2005 total_post_num = sqlite3_column_int64(s, 0);
2006 break;
2007 default:
2008 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
2009 goto done;
2012 sqlite3_reset(s2);
2013 sqlite3_clear_bindings(s2);
2014 TRY_BIND_I(s2, db, "@thread", thread_id);
2015 again:
2016 sret = sqlite3_step(s2);
2018 switch (sret) {
2019 case SQLITE_DONE:
2020 break;
2021 case SQLITE_ROW:
2022 p = (struct prepared_post) { 0 };
2023 p.id = sqlite3_column_int64(s2, 0);
2024 p.now = sqlite3_column_int64(s2, 1);
2025 EXFILTRATE_TEXT(s2, 2, p.name, p.name_len);
2026 EXFILTRATE_TEXT(s2, 3, p.subject, p.subject_len);
2027 EXFILTRATE_TEXT(s2, 4, p.email, p.email_len);
2028 EXFILTRATE_TEXT(s2, 5, p.tripcode, p.tripcode_len);
2029 EXFILTRATE_TEXT(s2, 6, p.comment, p.comment_len);
2030 EXFILTRATE_TEXT(s2, 7, p.file_name, p.file_name_len);
2031 EXFILTRATE_TEXT(s2, 8, p.system_full_path,
2032 p.system_full_path_len);
2033 EXFILTRATE_TEXT(s2, 9, p.system_thumb_path,
2034 p.system_thumb_path_len);
2035 EXFILTRATE_TEXT(s2, 10, p.file_info, p.file_info_len);
2036 EXFILTRATE_TEXT(s2, 11, p.ip, p.ip_len);
2037 p.thread_closed = !!sqlite3_column_int64(s2, 12);
2038 p.thread_stickied = !!sqlite3_column_int64(s2, 13);
2039 wt_write_post(&p, f, first_post, 1, 0,
2040 conf->boards[board_idx].name, 0,
2041 total_post_num,
2042 first_post);
2043 first_post = 0;
2044 clean_prepared_post(&p);
2045 goto again;
2046 default:
2047 ERROR_MESSAGE("sqlite3_step(): %s", sqlite3_errmsg(db));
2048 break;
2052 ret = 0;
2053 done:
2054 sqlite3_reset(s);
2055 sqlite3_clear_bindings(s);
2057 return ret;
2061 * Clean up any memory from this file
2063 * Postconditions (success):
2065 * - Valgrind won't report any memory leaks from this file.
2067 * - setup_dbs() can be safely called again.
2070 clean_dbs(void)
2072 /* Close board connections */
2073 for (size_t j = 0; j < num_connected_db; ++j) {
2074 FINALIZE_FOR_BOARD(j, check_ban);
2075 FINALIZE_FOR_BOARD(j, check_cooldown);
2076 FINALIZE_FOR_BOARD(j, check_thread_exists);
2077 FINALIZE_FOR_BOARD(j, count_posts);
2078 FINALIZE_FOR_BOARD(j, delete_thread);
2079 FINALIZE_FOR_BOARD(j, delete_post);
2080 FINALIZE_FOR_BOARD(j, find_containing_thread);
2081 FINALIZE_FOR_BOARD(j, get_subject);
2082 FINALIZE_FOR_BOARD(j, get_thread_contents);
2083 FINALIZE_FOR_BOARD(j, get_thread_summary);
2084 FINALIZE_FOR_BOARD(j, get_post_contents);
2085 FINALIZE_FOR_BOARD(j, insert_ban);
2086 FINALIZE_FOR_BOARD(j, insert_comment);
2087 FINALIZE_FOR_BOARD(j, insert_comment_II);
2088 FINALIZE_FOR_BOARD(j, list_threads);
2089 FINALIZE_FOR_BOARD(j, set_cooldown);
2090 FINALIZE_FOR_BOARD(j, update_by_moderation);
2091 FINALIZE_FOR_BOARD(j, update_file_info);
2092 FINALIZE_FOR_BOARD(j, get_recent_posts);
2094 if (board_dbs) {
2095 sqlite3_close(board_dbs[j]);
2098 board_dbs[j] = 0;
2101 free(board_dbs);
2102 board_dbs = 0;
2103 conf = 0;
2105 /* Clean up prepared board statements */
2106 CLEAN_UP_STATEMENT_ARRAY(check_ban);
2107 CLEAN_UP_STATEMENT_ARRAY(check_cooldown);
2108 CLEAN_UP_STATEMENT_ARRAY(check_thread_exists);
2109 CLEAN_UP_STATEMENT_ARRAY(count_posts);
2110 CLEAN_UP_STATEMENT_ARRAY(delete_thread);
2111 CLEAN_UP_STATEMENT_ARRAY(delete_post);
2112 CLEAN_UP_STATEMENT_ARRAY(find_containing_thread);
2113 CLEAN_UP_STATEMENT_ARRAY(get_subject);
2114 CLEAN_UP_STATEMENT_ARRAY(get_thread_contents);
2115 CLEAN_UP_STATEMENT_ARRAY(get_thread_summary);
2116 CLEAN_UP_STATEMENT_ARRAY(get_post_contents);
2117 CLEAN_UP_STATEMENT_ARRAY(insert_ban);
2118 CLEAN_UP_STATEMENT_ARRAY(insert_comment);
2119 CLEAN_UP_STATEMENT_ARRAY(insert_comment_II);
2120 CLEAN_UP_STATEMENT_ARRAY(list_threads);
2121 CLEAN_UP_STATEMENT_ARRAY(set_cooldown);
2122 CLEAN_UP_STATEMENT_ARRAY(update_by_moderation);
2123 CLEAN_UP_STATEMENT_ARRAY(update_file_info);
2124 CLEAN_UP_STATEMENT_ARRAY(get_recent_posts);
2126 /* Close global connection */
2127 sqlite3_finalize(global_check_ban_stmt);
2128 global_check_ban_stmt = 0;
2129 sqlite3_finalize(global_insert_ban_stmt);
2130 global_insert_ban_stmt = 0;
2132 if (global_db) {
2133 sqlite3_close(global_db);
2134 global_db = 0;
2137 return 0;