misc: correctly proceed to thread-deletion in rb79-delete-post
[rb-79.git] / write-thread.c
blobbf103b8700c44a569cae87139de18871f07e2a32
1 /*
2 * Copyright (c) 2017-2018, 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 <errno.h>
19 #include <stddef.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <strings.h>
24 #include <sys/stat.h>
25 #include <time.h>
26 #include <unistd.h>
28 #define PCRE2_CODE_UNIT_WIDTH 8
29 #include <pcre2.h>
31 #include "macros.h"
32 #include "rb79.h"
34 /* Global configuration */
35 const struct configuration *conf;
37 /* A template for pages 1, 2, ... of every board */
38 static char *board_page;
39 static size_t board_page_len;
41 /* A template for a particular thread's page */
42 static char *thread_page;
43 static size_t thread_page_len;
45 /* A template for the /recent/index.html page */
46 static char *recent_page;
47 static size_t recent_page_len;
49 /* PCRE2 for parsing templates (pull out ${FOO}s) */
50 static pcre2_code *variables;
52 #define CHECK_FPRINTF(...) \
53 do { \
54 if (fprintf(__VA_ARGS__) < 0) { \
55 PERROR_MESSAGE("fprintf"); \
56 goto done; \
57 } \
58 } while (0)
61 * Read in the contents of ${static_www_folder}/_templates/${local_name}
62 * to *out.
64 * Preconditions:
66 * - ${static_www_folder}/_templates/${local_name} is a readable folder.
68 * - out is not 0.
70 * - Overwriting *out shall not cause a memory leak.
72 * Postconditions (success):
74 * - *out contains the full contents of
75 * ${static_www_folder}/_templates/${local_name}.
77 static int slurp_file(const char *local_name, char **out)
79 int ret = -1;
80 FILE *f = 0;
81 char *path = 0;
82 size_t path_len = 0;
83 size_t len = 0;
85 /* Load board_page */
86 path_len = snprintf(0, 0, "%s/_templates/%s", conf->static_www_folder,
87 local_name);
89 if (path_len + 1 < path_len) {
90 ERROR_MESSAGE("overflow");
91 goto done;
94 if (!(path = malloc(path_len + 1))) {
95 PERROR_MESSAGE("malloc");
96 goto done;
99 sprintf(path, "%s/_templates/%s", conf->static_www_folder, local_name);
101 if (!(f = fopen(path, "r"))) {
102 ERROR_MESSAGE("cannot open \"%s\"", path);
103 PERROR_MESSAGE("fopen");
104 goto done;
107 fseek(f, 0, SEEK_END);
108 len = ftell(f);
110 if (len + 1 < len) {
111 ERROR_MESSAGE("overflow");
112 goto done;
115 if (!(*out = malloc(len + 1))) {
116 PERROR_MESSAGE("malloc");
117 goto done;
120 fseek(f, 0, SEEK_SET);
122 if (fread(*out, 1, len, f) < len) {
123 /* Short read - either error or file was modified under us */
124 PERROR_MESSAGE("fread");
125 goto done;
128 (*out)[len] = 0;
130 ret = 0;
131 done:
133 if (f) {
134 if (fclose(f)) {
135 PERROR_MESSAGE("fclose");
136 ret = -1;
140 free(path);
142 return ret;
146 * Initialize any static elements needed for this file
148 * Preconditions:
150 * - setup_write_thread() was not invoked more recently than
151 * clean_write_thread().
153 * Postconditions (success):
155 * - Any other function in this file may be safely called.
157 int setup_write_thread(const struct configuration *in_conf)
159 conf = in_conf;
161 if (slurp_file("board_page", &board_page) < 0) {
162 return -1;
165 if (!board_page) {
166 ERROR_MESSAGE("Could not read board page template");
167 return -1;
170 board_page_len = strlen(board_page);
172 if (slurp_file("thread_page", &thread_page) < 0) {
173 return -1;
176 thread_page_len = strlen(thread_page);
178 if (slurp_file("recent_page", &recent_page) < 0) {
179 return -1;
182 recent_page_len = strlen(recent_page);
183 int err_code = 0;
184 PCRE2_SIZE err_offset = 0;
185 PCRE2_UCHAR8 err_buf[120];
186 const char *variable_pattern_str = "[$][{](?<var>[^}]+)[}]";
188 if (!(variables = pcre2_compile((PCRE2_SPTR8) variable_pattern_str,
189 PCRE2_ZERO_TERMINATED, PCRE2_UTF,
190 &err_code, &err_offset, 0))) {
191 pcre2_get_error_message(err_code, err_buf, 120);
192 ERROR_MESSAGE("pcre2_compile: error with pattern \"%s\": %s",
193 variable_pattern_str, err_buf);
195 return -1;
198 return 0;
202 * Delete files which are in static_www_folder. Note that "full"
203 * means "the full file", not "the full path to the file". This is
204 * a defect.
206 * Preconditions:
208 * - system_full_path is a string of length system_full_path_len,
209 * something like "/m/src/4921183834.png".
211 * - system_thumb_path is a string of length system_thumb_path_len,
212 * something like "/m/src/4921183834.png".
214 * - As an exception, system_full_path and system_thumb_path may
215 * be 0 if their respective lengths are 0.
217 * - There is at most one board to which system_full_path and
218 * system_thumb_path are associated. For that board, THE LOCK
219 * IS HELD.
221 * Postconditions (success):
223 * - Those files have been unlink()'d.
225 int wt_remove_files(const char *system_full_path, size_t system_full_path_len,
226 const char *system_thumb_path, size_t system_thumb_path_len)
228 int ret = -1;
229 char *abs_full_path = 0;
230 size_t abs_full_path_len = 0;
231 char *abs_thumb_path = 0;
232 size_t abs_thumb_path_len = 0;
233 size_t j = 0;
235 if (!system_full_path_len) {
236 goto done_with_full;
239 abs_full_path_len = snprintf(0, 0, "%s%s", conf->static_www_folder,
240 system_full_path);
242 if (abs_full_path_len + 1 < abs_full_path_len) {
243 ERROR_MESSAGE("overflow");
244 goto done;
247 if (!(abs_full_path = malloc(abs_full_path_len + 1))) {
248 PERROR_MESSAGE("malloc");
249 goto done;
252 sprintf(abs_full_path, "%s%s", conf->static_www_folder,
253 system_full_path);
255 if (unlink(abs_full_path) < 0) {
256 PERROR_MESSAGE("unlink");
257 ERROR_MESSAGE("Failed to unlink(\"%s\")", abs_full_path);
258 goto done;
261 done_with_full:
263 if (!system_thumb_path_len) {
264 goto done_with_thumb;
267 for (j = 0; j < conf->filetypes_num; ++j) {
268 const struct filetype *f = &conf->filetypes[j];
270 if (f->static_thumbnail &&
271 !strcmp(f->static_thumbnail, system_thumb_path)) {
272 goto done_with_thumb;
276 abs_thumb_path_len = snprintf(0, 0, "%s%s", conf->static_www_folder,
277 system_thumb_path);
279 if (abs_thumb_path_len + 1 < abs_thumb_path_len) {
280 ERROR_MESSAGE("overflow");
281 goto done;
284 if (!(abs_thumb_path = malloc(abs_thumb_path_len + 1))) {
285 PERROR_MESSAGE("malloc");
286 goto done;
289 sprintf(abs_thumb_path, "%s%s", conf->static_www_folder,
290 system_thumb_path);
292 if (unlink(abs_thumb_path) < 0) {
293 PERROR_MESSAGE("unlink");
294 ERROR_MESSAGE("Failed to unlink(\"%s\")", abs_thumb_path);
295 goto done;
298 done_with_thumb:
299 ret = 0;
300 done:
301 free(abs_full_path);
302 free(abs_thumb_path);
304 return ret;
308 * Delete files which are in static_www_folder. Note that "full"
309 * means "the full file", not "the full path to the file". This is
310 * a defect.
312 * Preconditions:
314 * - board_idx represents a board, AND THE LOCK IS HELD.
316 * - There is a page for thread_id up, e.g. at
317 * ${static_www_folder}/m/res/${thread_id}/index.html.
319 * Postconditions (success):
321 * - The folder for thread id (e.g.
322 * ${static_www_folder}/m/res/${thread_id}) no longer exists.
324 int wt_remove_thread_page(size_t board_idx, uintmax_t thread_id)
326 int ret = -1;
327 char *index_path = 0;
328 char *folder_path = 0;
329 size_t len = snprintf(0, 0, "%s/%s/res/%ju/index.html",
330 conf->static_www_folder,
331 conf->boards[board_idx].name, thread_id);
333 if (len + 1 < len) {
334 ERROR_MESSAGE("overflow");
335 goto done;
338 if (!(index_path = malloc(len + 1))) {
339 PERROR_MESSAGE("malloc");
340 goto done;
343 sprintf(index_path, "%s/%s/res/%ju/index.html", conf->static_www_folder,
344 conf->boards[board_idx].name, thread_id);
346 if (unlink(index_path) < 0) {
347 PERROR_MESSAGE("unlink");
348 ERROR_MESSAGE("Failed to unlink(\"%s\")", index_path);
349 goto done;
352 if (!(folder_path = malloc(len + 1))) {
353 PERROR_MESSAGE("malloc");
354 goto done;
357 sprintf(folder_path, "%s/%s/res/%ju", conf->static_www_folder,
358 conf->boards[board_idx].name, thread_id);
360 if (rmdir(folder_path) < 0) {
361 PERROR_MESSAGE("rmdir");
362 ERROR_MESSAGE("Failed to rmdir(\"%s\")", folder_path);
363 goto done;
366 ret = 0;
367 done:
368 free(index_path);
369 free(folder_path);
371 return ret;
375 * Write out the HTML file for a board
377 * Preconditions:
379 * - setup_write_thread() has been called more recently than
380 * clean_write_thread().
382 * - board_idx represents a board, AND THE LOCK IS HELD.
384 * - thread_ids is an array of length thread_ids_num.
386 * - Each element of thread_ids represents a thread.
388 * - thread_ids is sorted by bump order, most recent thread first.
390 * Postconditions (success):
392 * - Files at ${static_www_folder}/${board}/${n}/index.html
393 * has been written out, representing the current state of the
394 * board thread, for n = 0 to however many there should be.
396 int wt_write_board(size_t board_idx, uintmax_t *thread_ids, size_t
397 thread_ids_num, size_t board_pages_num)
399 int ret = -1;
400 FILE *f = 0;
401 size_t len = 0;
402 size_t j = 0;
403 pcre2_match_data *match_data = 0;
404 PCRE2_UCHAR *var_name = 0;
405 uint_fast8_t more_left_to_delete = 1;
406 char *path = 0;
408 /* 3 capture groups: more than sufficient for ${foo} */
409 if (!(match_data = pcre2_match_data_create(3, 0))) {
410 PERROR_MESSAGE("pcre2_match_data_create");
411 goto done;
414 len = snprintf(0, 0, "%s/%s/%zu/index.html", conf->static_www_folder,
415 conf->boards[board_idx].name, (size_t) -1);
417 if (len + 1 < len) {
418 ERROR_MESSAGE("overflow");
419 goto done;
422 if (!(path = malloc(len + 1))) {
423 PERROR_MESSAGE("malloc");
424 goto done;
427 /* First, delete all the pages we won't need */
430 * XXX: should we really be rmdir()ing? Can't we just leave
431 * the old directories around?
433 j = board_pages_num ? board_pages_num : 1;
435 while (j != (size_t) -1 &&
436 more_left_to_delete) {
437 sprintf(path, "%s/%s/%zu/index.html", conf->static_www_folder,
438 conf->boards[board_idx].name, j);
440 if (unlink(path) < 0) {
441 if (errno == ENOENT) {
442 more_left_to_delete = 0;
443 break;
446 PERROR_MESSAGE("unlink");
447 ERROR_MESSAGE("Failed to unlink(\"%s\")", path);
448 goto done;
451 sprintf(path, "%s/%s/%zu", conf->static_www_folder,
452 conf->boards[board_idx].name, j);
454 if (rmdir(path) < 0) {
455 PERROR_MESSAGE("rmdir");
456 ERROR_MESSAGE("Failed to rmdir(\"%s\")", path);
457 goto done;
460 j++;
463 /* Now build the pages we do need */
464 for (size_t current_page = 0; !current_page ||
465 (current_page < board_pages_num); ++current_page) {
466 size_t idx = 0;
467 size_t match_pos = 0;
468 size_t after_match_pos = 0;
469 int nret = 0;
470 PCRE2_SIZE var_name_len = 0;
471 size_t challenge_id = rand() % conf->challenges_num;
472 size_t first_thread_idx = current_page *
473 conf->boards[board_idx].
474 threads_per_page;
475 size_t num_threads_this_page =
476 conf->boards[board_idx].threads_per_page;
478 if (first_thread_idx + num_threads_this_page >=
479 thread_ids_num) {
480 num_threads_this_page = thread_ids_num -
481 first_thread_idx;
484 if (current_page) {
485 sprintf(path, "%s/%s/%zu", conf->static_www_folder,
486 conf->boards[board_idx].name, current_page);
487 errno = 0;
489 /* Make the directory. We shouldn't need mkdir -p. */
490 if (mkdir(path, 0755) < 0) {
491 if (errno != EEXIST) {
492 PERROR_MESSAGE("mkdir");
493 ERROR_MESSAGE("mkdir(\"%s\") failed",
494 path);
495 goto done;
499 /* Now open the file */
500 sprintf(path, "%s/%s/%zu/index.html",
501 conf->static_www_folder,
502 conf->boards[board_idx].name,
503 current_page);
504 } else {
505 sprintf(path, "%s/%s/index.html",
506 conf->static_www_folder,
507 conf->boards[board_idx].name);
510 if (f) {
511 if (fclose(f)) {
512 PERROR_MESSAGE("fclose");
513 goto done;
517 if (!(f = fopen(path, "w"))) {
518 PERROR_MESSAGE("fopen");
519 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", path);
520 goto done;
523 find_next_bit:
525 /* Are we done? */
526 if (idx >= board_page_len) {
527 goto success;
530 /* Where does ${FOO} appear next? */
531 nret = pcre2_match(variables, (PCRE2_SPTR) board_page,
532 board_page_len, idx, 0, match_data, 0);
534 if (nret == PCRE2_ERROR_NOMATCH) {
535 CHECK_FPRINTF(f, "%s", board_page + idx);
536 goto success;
539 if (nret < 0) {
540 PCRE2_UCHAR8 err_buf[120];
542 pcre2_get_error_message(nret, err_buf, 120);
543 ERROR_MESSAGE(
544 "pcre2_match: error while matching \"%.*s\": %s"
545 " (PCRE2 %d)", (int) (board_page_len - idx),
546 board_page + idx, err_buf, nret);
547 goto done;
550 /* What is FOO? */
551 pcre2_substring_free(var_name);
552 var_name = 0;
553 var_name_len = 0;
555 if ((nret = pcre2_substring_get_byname(match_data,
556 (PCRE2_SPTR) "var",
557 &var_name,
558 &var_name_len))) {
559 PCRE2_UCHAR8 err_buf[120];
561 pcre2_get_error_message(nret, err_buf, 120);
562 ERROR_MESSAGE(
563 "pcre2_substring_get_byname: %s (PCRE2 %d)",
564 err_buf,
565 nret);
566 goto done;
569 match_pos = pcre2_get_ovector_pointer(match_data)[0];
570 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
571 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), board_page +
572 idx);
573 idx = match_pos;
575 if (!strncasecmp((const char *) var_name, "BOARD",
576 var_name_len)) {
577 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].name);
578 } else if (!strncasecmp((const char *) var_name, "BOARD_TITLE",
579 var_name_len)) {
580 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].title);
581 } else if (!strncasecmp((const char *) var_name, "CHALLENGE",
582 var_name_len)) {
583 CHECK_FPRINTF(f, "%s",
584 conf->challenges[challenge_id].question);
585 } else if (!strncasecmp((const char *) var_name, "CHALLENGE_ID",
586 var_name_len)) {
587 CHECK_FPRINTF(f, "%zu", challenge_id);
588 } else if (!strncasecmp((const char *) var_name, "PAGELINKS",
589 var_name_len)) {
590 CHECK_FPRINTF(f, "[<a href=\"/%s\">0</a>] ",
591 conf->boards[board_idx].name);
593 for (size_t j = 1; j < board_pages_num; ++j) {
594 CHECK_FPRINTF(f,
595 "[<a href=\"/%s/%zu\">%zu</a>] ",
596 conf->boards[board_idx].name, j,
599 } else if (!strncasecmp((const char *) var_name,
600 "RANDOM_HEADER", var_name_len)) {
601 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
602 conf->headers_num]);
603 } else if (!strncasecmp((const char *) var_name, "THREADS",
604 var_name_len)) {
606 * We have to transfer control to db-ZZZ
607 * for a bit - it comes back through
608 * wt_write_post().
610 db_writeback_thread_summaries(board_idx, thread_ids +
611 first_thread_idx,
612 num_threads_this_page, f);
613 } else {
614 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len,
615 var_name);
618 idx = after_match_pos;
619 goto find_next_bit;
620 success:
624 ret = 0;
625 done:
626 pcre2_match_data_free(match_data);
627 pcre2_substring_free(var_name);
629 if (f) {
630 if (fclose(f)) {
631 PERROR_MESSAGE("fclose");
632 ret = -1;
636 free(path);
638 return ret;
642 * Write p out to f as the first post of a thread.
644 * This function is intended to be called by
645 * db_writeback_posts_in_thread(), which is called from wt_write_thread().
647 * Preconditions:
649 * - setup_write_thread() has been called more recently than
650 * clean_write_thread().
652 * - p is a fully filled stored_post (i.e. from a row in the database).
654 * - f is an open, writable FILE *.
656 * - board_idx represents a baord.
658 * - If is_summary is not zero, then board_name is a string like
659 * "m" or "sf". Furthermore, if is_op is also not zero, then
660 * total_posts_in_thread is a number suitable for describing the
661 * thread.
663 * Postconditions (success):
665 * - the contents of p have been written to f, which returned no
666 * errors. The format is not precisely defined.
668 int wt_write_post(struct prepared_post *p, FILE *f, uint_fast8_t is_op,
669 uint_fast8_t is_summary, uint_fast8_t is_recent, const
670 char *board_name,
671 uintmax_t in_thread, uintmax_t total_posts_in_thread,
672 uint_fast8_t hr_before)
674 int ret = -1;
675 char *human_readable_time = util_iso8601_from_time_t(p->now);
677 if (!human_readable_time) {
678 ERROR_MESSAGE("util_iso_8601_from_time(%ju) failed",
679 (uintmax_t) p->now);
682 if (hr_before) {
683 /* XXX: make sure this looks decent, both in ff and elinks */
684 CHECK_FPRINTF(f, "<hr />");
687 if (is_op) {
688 CHECK_FPRINTF(f, "<div class=\"op-post\" id=\"post%ju\">",
689 p->id);
690 } else {
691 CHECK_FPRINTF(f, "<div class=\"reply-container\">");
693 if (is_recent) {
694 CHECK_FPRINTF(f, "<div class=\"full-link\">");
695 CHECK_FPRINTF(f, "<a class=\"a-subtle\" href=\"");
696 CHECK_FPRINTF(f, "/%s/res/%ju/#post%ju\">/%s/</a> ",
697 board_name, in_thread, p->id, board_name);
698 CHECK_FPRINTF(f, "</div>");
699 } else {
700 CHECK_FPRINTF(f,
701 "<div class=\"reply-spacer\">..</div>");
704 CHECK_FPRINTF(f, "<div class=\"reply-post\" id=\"post%ju\">",
705 p->id);
708 CHECK_FPRINTF(f, "<span class=\"post-info\">");
710 if (p->subject_len) {
711 CHECK_FPRINTF(f, "<span class=\"post-subject\">%.*s</span> ",
712 (int) p->subject_len, p->subject);
715 if (p->name_len) {
716 if (p->email_len) {
717 CHECK_FPRINTF(f, "<a href=\"mailto:%.*s\">",
718 (int) p->email_len, p->email);
721 CHECK_FPRINTF(f, "<span class=\"post-author\">%.*s</span>",
722 (int) p->name_len, p->name);
724 if (p->email_len) {
725 CHECK_FPRINTF(f, "</a>");
728 if (p->tripcode_len) {
729 CHECK_FPRINTF(f, "<span class=\"post-tripcode\">");
730 CHECK_FPRINTF(f, "!%.*s</span>", (int) p->tripcode_len,
731 p->tripcode);
735 CHECK_FPRINTF(f, " <span class=\"post-time\">%s</span>",
736 human_readable_time ? human_readable_time : "NEVER");
737 CHECK_FPRINTF(f, " <span class=\"post-number\">No. %ju</span>", p->id);
739 if (is_op) {
740 if (p->thread_closed) {
741 CHECK_FPRINTF(f, " <span class=\"thread-closed\">");
742 CHECK_FPRINTF(f, "Closed</span>");
745 if (p->thread_stickied) {
746 CHECK_FPRINTF(f, " <span class=\"thread-stickied\">");
747 CHECK_FPRINTF(f, "Sticky</span>");
751 if (is_summary &&
752 is_op) {
753 CHECK_FPRINTF(f, " [<a href=\"/%s/res/%ju\">View</a>]",
754 board_name, p->id);
757 CHECK_FPRINTF(f, "</span><br />");
759 if (p->system_full_path_len) {
760 CHECK_FPRINTF(f, "<span class=\"file-info\">");
761 CHECK_FPRINTF(f, "<a class=\"filelink\" href=\"%.*s\">",
762 (int) p->system_full_path_len,
763 p->system_full_path);
764 CHECK_FPRINTF(f, "%.*s", (int) p->file_name_len, p->file_name);
765 CHECK_FPRINTF(f, "</a>");
766 CHECK_FPRINTF(f, " (%.*s)</span><br />", (int) p->file_info_len,
767 p->file_info);
768 CHECK_FPRINTF(f, "<div class=\"post-image\">");
769 CHECK_FPRINTF(f, "<a class=\"filelink\" href=\"%.*s\">",
770 (int) p->system_full_path_len,
771 p->system_full_path);
772 CHECK_FPRINTF(f, "<img src=\"%.*s\" alt=\"image\" ",
773 (int) p->system_thumb_path_len,
774 p->system_thumb_path);
775 CHECK_FPRINTF(f, "class=\"thumb\"/>");
776 CHECK_FPRINTF(f, "</a></div>");
779 CHECK_FPRINTF(f, "<div class=\"post-comment\">");
781 if (p->comment_len) {
782 CHECK_FPRINTF(f, "%.*s", (int) p->comment_len, p->comment);
783 } else {
784 CHECK_FPRINTF(f, "&nbsp;");
787 CHECK_FPRINTF(f, "</div>");
788 CHECK_FPRINTF(f, "</div>");
790 if (!is_op) {
791 CHECK_FPRINTF(f, "</div>");
794 if (is_summary &&
795 is_op) {
796 CHECK_FPRINTF(f, "<div class=\"reply-count-info\">");
797 CHECK_FPRINTF(f, "%ju post%s in thread. ",
798 total_posts_in_thread, (total_posts_in_thread ==
799 1) ? "" : "s");
800 CHECK_FPRINTF(f, "<a href=\"/%s/res/%ju\">View thread</a>",
801 board_name, p->id);
802 CHECK_FPRINTF(f, "</div>");
805 ret = 0;
806 done:
807 free(human_readable_time);
809 return ret;
813 * Write out the HTML for the /recent/ page
815 * Preconditions:
817 * - setup_write_thread() has been called more recently than
818 * clean_write_thread().
820 * - for the recent page, THE LOCK IS HELD.
822 * Postconditions (success):
824 * - A file at ${static_www_folder}/recent/index.html has been
825 * written out, representing the last few posts on the site.
827 int wt_write_recent_page(void)
829 int ret = -1;
830 size_t idx = 0;
831 size_t match_pos = 0;
832 size_t after_match_pos = 0;
833 pcre2_match_data *match_data = 0;
834 int nret = 0;
835 char *path = 0;
836 size_t len = 0;
837 FILE *f = 0;
838 PCRE2_UCHAR *var_name = 0;
839 PCRE2_SIZE var_name_len = 0;
841 len = snprintf(0, 0, "%s/recent/index.html", conf->static_www_folder);
843 if (len + 1 < len) {
844 ERROR_MESSAGE("overflow");
845 goto done;
848 if (!(path = malloc(len + 1))) {
849 PERROR_MESSAGE("malloc");
850 goto done;
853 sprintf(path, "%s/recent", conf->static_www_folder);
855 /* Make the directory. We shouldn't need mkdir -p. */
856 if (mkdir(path, 0755) < 0) {
857 if (errno != EEXIST) {
858 PERROR_MESSAGE("mkdir");
859 ERROR_MESSAGE("mkdir(\"%s\") failed", path);
860 goto done;
864 sprintf(path, "%s/recent/index.html", conf->static_www_folder);
866 if (!(f = fopen(path, "w"))) {
867 PERROR_MESSAGE("fopen");
868 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", path);
869 goto done;
872 /* 3 capture groups: more than sufficient for ${foo} */
873 if (!(match_data = pcre2_match_data_create(3, 0))) {
874 PERROR_MESSAGE("pcre2_match_data_create");
875 goto done;
878 find_next_bit:
880 /* Are we done? */
881 if (idx >= recent_page_len) {
882 goto success;
885 /* Where does ${FOO} appear next? */
886 nret = pcre2_match(variables, (PCRE2_SPTR) recent_page, recent_page_len,
887 idx, 0, match_data, 0);
889 if (nret == PCRE2_ERROR_NOMATCH) {
890 CHECK_FPRINTF(f, "%s", recent_page + idx);
891 goto success;
894 if (nret < 0) {
895 PCRE2_UCHAR8 err_buf[120];
897 pcre2_get_error_message(nret, err_buf, 120);
898 ERROR_MESSAGE("pcre2_match: error while matching \"%.*s\": %s"
899 " (PCRE2 %d)", (int) (recent_page_len - idx),
900 recent_page + idx,
901 err_buf, nret);
902 goto done;
905 /* What is FOO? */
906 pcre2_substring_free(var_name);
907 var_name = 0;
908 var_name_len = 0;
910 if ((nret = pcre2_substring_get_byname(match_data, (PCRE2_SPTR) "var",
911 &var_name, &var_name_len))) {
912 PCRE2_UCHAR8 err_buf[120];
914 pcre2_get_error_message(nret, err_buf, 120);
915 ERROR_MESSAGE("pcre2_substring_get_byname: %s (PCRE2 %d)",
916 err_buf, nret);
917 goto done;
920 match_pos = pcre2_get_ovector_pointer(match_data)[0];
921 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
922 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), recent_page + idx);
923 idx = match_pos;
925 if (!strncasecmp((const char *) var_name, "RANDOM_HEADER",
926 var_name_len)) {
927 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
928 conf->headers_num]);
929 } else if (!strncasecmp((const char *) var_name, "RECENT_POSTS",
930 var_name_len)) {
932 * We have to transfer control to db-ZZZ for a bit
933 * - it comes back through wt_write_post().
935 db_writeback_recent_posts(f, wt_write_post);
936 } else {
937 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len, var_name);
940 idx = after_match_pos;
941 goto find_next_bit;
942 success:
943 ret = 0;
944 done:
945 pcre2_match_data_free(match_data);
946 pcre2_substring_free(var_name);
948 if (f) {
949 if (fclose(f)) {
950 PERROR_MESSAGE("fclose");
951 ret = -1;
955 free(path);
957 return ret;
961 * Write out the HTML file for a thread
963 * Preconditions:
965 * - setup_write_thread() has been called more recently than
966 * clean_write_thread().
968 * - board_idx represents a board, AND THE LOCK IS HELD.
970 * - thread is the post_id of a thread.
972 * Postconditions (success):
974 * - A file at ${static_www_folder}/${board}/res/${thread}/index.html
975 * has been written out, representing the recent state of the
976 * thread.
978 int wt_write_thread(size_t board_idx, uintmax_t thread)
980 int ret = -1;
981 size_t idx = 0;
982 size_t match_pos = 0;
983 size_t after_match_pos = 0;
984 pcre2_match_data *match_data = 0;
985 int nret = 0;
986 FILE *f = 0;
987 size_t len = 0;
988 char *index_path = 0;
989 PCRE2_UCHAR *var_name = 0;
990 PCRE2_SIZE var_name_len = 0;
991 size_t challenge_id = rand() % conf->challenges_num;
993 len = snprintf(0, 0, "%s/%s/res/%ju/index.html",
994 conf->static_www_folder, conf->boards[board_idx].name,
995 thread);
997 if (len + 1 < len) {
998 ERROR_MESSAGE("overflow");
999 goto done;
1002 if (!(index_path = malloc(len + 1))) {
1003 PERROR_MESSAGE("malloc");
1004 goto done;
1007 sprintf(index_path, "%s/%s/res/%ju", conf->static_www_folder,
1008 conf->boards[board_idx].name, thread);
1009 errno = 0;
1011 /* Make the directory. We shouldn't need mkdir -p. */
1012 if (mkdir(index_path, 0755) < 0) {
1013 if (errno != EEXIST) {
1014 PERROR_MESSAGE("mkdir");
1015 ERROR_MESSAGE("mkdir(\"%s\") failed", index_path);
1016 goto done;
1020 /* Now open the file */
1021 sprintf(index_path, "%s/%s/res/%ju/index.html", conf->static_www_folder,
1022 conf->boards[board_idx].name, thread);
1024 if (!(f = fopen(index_path, "w"))) {
1025 PERROR_MESSAGE("fopen");
1026 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", index_path);
1027 goto done;
1030 /* 3 capture groups: more than sufficient for ${foo} */
1031 if (!(match_data = pcre2_match_data_create(3, 0))) {
1032 PERROR_MESSAGE("pcre2_match_data_create");
1033 goto done;
1036 find_next_bit:
1038 /* Are we done? */
1039 if (idx >= thread_page_len) {
1040 goto success;
1043 /* Where does ${FOO} appear next? */
1044 nret = pcre2_match(variables, (PCRE2_SPTR) thread_page, thread_page_len,
1045 idx, 0, match_data, 0);
1047 if (nret == PCRE2_ERROR_NOMATCH) {
1048 CHECK_FPRINTF(f, "%s", thread_page + idx);
1049 goto success;
1052 if (nret < 0) {
1053 PCRE2_UCHAR8 err_buf[120];
1055 pcre2_get_error_message(nret, err_buf, 120);
1056 ERROR_MESSAGE("pcre2_match: error while matching \"%.*s\": %s"
1057 " (PCRE2 %d)", (int) (thread_page_len - idx),
1058 thread_page + idx,
1059 err_buf, nret);
1060 goto done;
1063 /* What is FOO? */
1064 pcre2_substring_free(var_name);
1065 var_name = 0;
1066 var_name_len = 0;
1068 if ((nret = pcre2_substring_get_byname(match_data, (PCRE2_SPTR) "var",
1069 &var_name, &var_name_len))) {
1070 PCRE2_UCHAR8 err_buf[120];
1072 pcre2_get_error_message(nret, err_buf, 120);
1073 ERROR_MESSAGE("pcre2_substring_get_byname: %s (PCRE2 %d)",
1074 err_buf, nret);
1075 goto done;
1078 match_pos = pcre2_get_ovector_pointer(match_data)[0];
1079 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
1080 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), thread_page + idx);
1081 idx = match_pos;
1083 if (!strncasecmp((const char *) var_name, "BOARD", var_name_len)) {
1084 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].name);
1085 } else if (!strncasecmp((const char *) var_name, "BOARD_TITLE",
1086 var_name_len)) {
1087 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].title);
1088 } else if (!strncasecmp((const char *) var_name, "CHALLENGE",
1089 var_name_len)) {
1090 CHECK_FPRINTF(f, "%s", conf->challenges[challenge_id].question);
1091 } else if (!strncasecmp((const char *) var_name, "CHALLENGE_ID",
1092 var_name_len)) {
1093 CHECK_FPRINTF(f, "%zu", challenge_id);
1094 } else if (!strncasecmp((const char *) var_name, "OP_SUBJECT",
1095 var_name_len)) {
1096 char *subject = 0;
1097 size_t subject_len = 0;
1099 if (db_extract_subject(board_idx, thread, &subject,
1100 &subject_len) < 0) {
1101 goto done;
1104 if (subject) {
1105 CHECK_FPRINTF(f, "%.*s", (int) subject_len, subject);
1108 free(subject);
1109 } else if (!strncasecmp((const char *) var_name, "RANDOM_HEADER",
1110 var_name_len)) {
1111 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
1112 conf->headers_num]);
1113 } else if (!strncasecmp((const char *) var_name, "THREAD_NUMBER",
1114 var_name_len)) {
1115 CHECK_FPRINTF(f, "%ju", thread);
1116 } else if (!strncasecmp((const char *) var_name, "POSTS",
1117 var_name_len)) {
1119 * We have to transfer control to db-ZZZ for a bit
1120 * - it comes back through wt_write_post().
1122 db_writeback_posts_in_thread(board_idx, thread, f,
1123 wt_write_post);
1124 } else {
1125 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len, var_name);
1128 idx = after_match_pos;
1129 goto find_next_bit;
1130 success:
1131 ret = 0;
1132 done:
1133 pcre2_match_data_free(match_data);
1134 pcre2_substring_free(var_name);
1136 if (f) {
1137 if (fclose(f)) {
1138 PERROR_MESSAGE("fclose");
1139 ret = -1;
1143 free(index_path);
1145 return ret;
1149 * Clean any memory from this file
1151 * Postconditions (success):
1153 * - Valgrind won't report any memory leaks from this file.
1155 * - setup_write_thread() can be safely called again.
1157 void clean_write_thread(void)
1159 conf = 0;
1160 free(board_page);
1161 free(thread_page);
1162 board_page = 0;
1163 board_page_len = 0;
1164 thread_page = 0;
1165 thread_page_len = 0;
1166 pcre2_code_free(variables);
1167 variables = 0;