server: handle pcre2 now returning -1 for "no match"
[rb-79.git] / write-thread.c
blob5f2f9d7af9f27e7c59ac30649dca80a4965328b5
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 <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 static 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
78 slurp_file(const char *local_name, char **out)
80 int ret = -1;
81 FILE *f = 0;
82 char *path = 0;
83 size_t path_len = 0;
84 size_t len = 0;
86 /* Load board_page */
87 path_len = snprintf(0, 0, "%s/_templates/%s", conf->static_www_folder,
88 local_name);
90 if (path_len + 1 < path_len) {
91 ERROR_MESSAGE("overflow");
92 goto done;
95 if (!(path = malloc(path_len + 1))) {
96 PERROR_MESSAGE("malloc");
97 goto done;
100 sprintf(path, "%s/_templates/%s", conf->static_www_folder, local_name);
102 if (!(f = fopen(path, "r"))) {
103 ERROR_MESSAGE("cannot open \"%s\"", path);
104 PERROR_MESSAGE("fopen");
105 goto done;
108 fseek(f, 0, SEEK_END);
109 len = ftell(f);
111 if (len + 1 < len) {
112 ERROR_MESSAGE("overflow");
113 goto done;
116 if (!(*out = malloc(len + 1))) {
117 PERROR_MESSAGE("malloc");
118 goto done;
121 fseek(f, 0, SEEK_SET);
123 if (fread(*out, 1, len, f) < len) {
124 /* Short read - either error or file was modified under us */
125 PERROR_MESSAGE("fread");
126 goto done;
129 (*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.
158 setup_write_thread(const struct configuration *in_conf)
160 conf = in_conf;
162 if (slurp_file("board_page", &board_page) < 0) {
163 return -1;
166 if (!board_page) {
167 ERROR_MESSAGE("Could not read board page template");
169 return -1;
172 board_page_len = strlen(board_page);
174 if (slurp_file("thread_page", &thread_page) < 0) {
175 return -1;
178 thread_page_len = strlen(thread_page);
180 if (slurp_file("recent_page", &recent_page) < 0) {
181 return -1;
184 recent_page_len = strlen(recent_page);
185 int err_code = 0;
186 PCRE2_SIZE err_offset = 0;
187 PCRE2_UCHAR8 err_buf[120];
188 const char *variable_pattern_str = "[$][{](?<var>[^}]+)[}]";
190 if (!(variables = pcre2_compile((PCRE2_SPTR8) variable_pattern_str,
191 PCRE2_ZERO_TERMINATED, PCRE2_UTF,
192 &err_code, &err_offset, 0))) {
193 pcre2_get_error_message(err_code, err_buf, 120);
194 ERROR_MESSAGE("pcre2_compile: error with pattern \"%s\": %s",
195 variable_pattern_str, err_buf);
197 return -1;
200 return 0;
204 * Delete files which are in static_www_folder. Note that "full"
205 * means "the full file", not "the full path to the file". This is
206 * a defect.
208 * Preconditions:
210 * - system_full_path is a string of length system_full_path_len,
211 * something like "/m/src/4921183834.png".
213 * - system_thumb_path is a string of length system_thumb_path_len,
214 * something like "/m/src/4921183834.png".
216 * - As an exception, system_full_path and system_thumb_path may
217 * be 0 if their respective lengths are 0.
219 * - There is at most one board to which system_full_path and
220 * system_thumb_path are associated. For that board, THE LOCK
221 * IS HELD.
223 * Postconditions (success):
225 * - Those files have been unlink()'d.
228 wt_remove_files(const char *system_full_path, size_t system_full_path_len, const
229 char *system_thumb_path, size_t system_thumb_path_len)
231 int ret = -1;
232 char *abs_full_path = 0;
233 size_t abs_full_path_len = 0;
234 char *abs_thumb_path = 0;
235 size_t abs_thumb_path_len = 0;
236 size_t j = 0;
238 if (!system_full_path_len) {
239 goto done_with_full;
242 abs_full_path_len = snprintf(0, 0, "%s%s", conf->static_www_folder,
243 system_full_path);
245 if (abs_full_path_len + 1 < abs_full_path_len) {
246 ERROR_MESSAGE("overflow");
247 goto done;
250 if (!(abs_full_path = malloc(abs_full_path_len + 1))) {
251 PERROR_MESSAGE("malloc");
252 goto done;
255 sprintf(abs_full_path, "%s%s", conf->static_www_folder,
256 system_full_path);
258 if (unlink(abs_full_path) < 0) {
259 PERROR_MESSAGE("unlink");
260 ERROR_MESSAGE("Failed to unlink(\"%s\")", abs_full_path);
261 goto done;
264 done_with_full:
266 if (!system_thumb_path_len) {
267 goto done_with_thumb;
270 for (j = 0; j < conf->filetypes_num; ++j) {
271 const struct filetype *f = &conf->filetypes[j];
273 if (f->static_thumbnail &&
274 !strcmp(f->static_thumbnail, system_thumb_path)) {
275 goto done_with_thumb;
279 abs_thumb_path_len = snprintf(0, 0, "%s%s", conf->static_www_folder,
280 system_thumb_path);
282 if (abs_thumb_path_len + 1 < abs_thumb_path_len) {
283 ERROR_MESSAGE("overflow");
284 goto done;
287 if (!(abs_thumb_path = malloc(abs_thumb_path_len + 1))) {
288 PERROR_MESSAGE("malloc");
289 goto done;
292 sprintf(abs_thumb_path, "%s%s", conf->static_www_folder,
293 system_thumb_path);
295 if (unlink(abs_thumb_path) < 0) {
296 PERROR_MESSAGE("unlink");
297 ERROR_MESSAGE("Failed to unlink(\"%s\")", abs_thumb_path);
298 goto done;
301 done_with_thumb:
302 ret = 0;
303 done:
304 free(abs_full_path);
305 free(abs_thumb_path);
307 return ret;
311 * Delete files which are in static_www_folder. Note that "full"
312 * means "the full file", not "the full path to the file". This is
313 * a defect.
315 * Preconditions:
317 * - board_idx represents a board, AND THE LOCK IS HELD.
319 * - There is a page for thread_id up, e.g. at
320 * ${static_www_folder}/m/res/${thread_id}/index.html.
322 * Postconditions (success):
324 * - The folder for thread id (e.g.
325 * ${static_www_folder}/m/res/${thread_id}) no longer exists.
328 wt_remove_thread_page(size_t board_idx, uintmax_t thread_id)
330 int ret = -1;
331 char *index_path = 0;
332 char *folder_path = 0;
333 size_t len = snprintf(0, 0, "%s/%s/res/%ju/index.html",
334 conf->static_www_folder,
335 conf->boards[board_idx].name, thread_id);
337 if (len + 1 < len) {
338 ERROR_MESSAGE("overflow");
339 goto done;
342 if (!(index_path = malloc(len + 1))) {
343 PERROR_MESSAGE("malloc");
344 goto done;
347 sprintf(index_path, "%s/%s/res/%ju/index.html", conf->static_www_folder,
348 conf->boards[board_idx].name, thread_id);
350 if (unlink(index_path) < 0) {
351 PERROR_MESSAGE("unlink");
352 ERROR_MESSAGE("Failed to unlink(\"%s\")", index_path);
353 goto done;
356 if (!(folder_path = malloc(len + 1))) {
357 PERROR_MESSAGE("malloc");
358 goto done;
361 sprintf(folder_path, "%s/%s/res/%ju", conf->static_www_folder,
362 conf->boards[board_idx].name, thread_id);
364 if (rmdir(folder_path) < 0) {
365 PERROR_MESSAGE("rmdir");
366 ERROR_MESSAGE("Failed to rmdir(\"%s\")", folder_path);
367 goto done;
370 ret = 0;
371 done:
372 free(index_path);
373 free(folder_path);
375 return ret;
379 * Write out the HTML file for a board
381 * Preconditions:
383 * - setup_write_thread() has been called more recently than
384 * clean_write_thread().
386 * - board_idx represents a board, AND THE LOCK IS HELD.
388 * - thread_ids is an array of length thread_ids_num.
390 * - Each element of thread_ids represents a thread.
392 * - thread_ids is sorted by bump order, most recent thread first.
394 * Postconditions (success):
396 * - Files at ${static_www_folder}/${board}/${n}/index.html
397 * has been written out, representing the current state of the
398 * board thread, for n = 0 to however many there should be.
401 wt_write_board(size_t board_idx, uintmax_t *thread_ids, size_t thread_ids_num,
402 size_t board_pages_num)
404 int ret = -1;
405 FILE *f = 0;
406 size_t len = 0;
407 size_t j = 0;
408 pcre2_match_data *match_data = 0;
409 PCRE2_UCHAR *var_name = 0;
410 uint_fast8_t more_left_to_delete = 1;
411 char *path = 0;
413 /* 3 capture groups: more than sufficient for ${foo} */
414 if (!(match_data = pcre2_match_data_create(3, 0))) {
415 PERROR_MESSAGE("pcre2_match_data_create");
416 goto done;
419 len = snprintf(0, 0, "%s/%s/%zu/index.html", conf->static_www_folder,
420 conf->boards[board_idx].name, (size_t) -1);
422 if (len + 1 < len) {
423 ERROR_MESSAGE("overflow");
424 goto done;
427 if (!(path = malloc(len + 1))) {
428 PERROR_MESSAGE("malloc");
429 goto done;
432 /* First, delete all the pages we won't need */
435 * XXX: should we really be rmdir()ing? Can't we just leave
436 * the old directories around?
438 j = board_pages_num ? board_pages_num : 1;
440 while (j != (size_t) -1 &&
441 more_left_to_delete) {
442 sprintf(path, "%s/%s/%zu/index.html", conf->static_www_folder,
443 conf->boards[board_idx].name, j);
445 if (unlink(path) < 0) {
446 if (errno == ENOENT) {
447 more_left_to_delete = 0;
448 break;
451 PERROR_MESSAGE("unlink");
452 ERROR_MESSAGE("Failed to unlink(\"%s\")", path);
453 goto done;
456 sprintf(path, "%s/%s/%zu", conf->static_www_folder,
457 conf->boards[board_idx].name, j);
459 if (rmdir(path) < 0) {
460 PERROR_MESSAGE("rmdir");
461 ERROR_MESSAGE("Failed to rmdir(\"%s\")", path);
462 goto done;
465 j++;
468 /* Now build the pages we do need */
469 for (size_t current_page = 0; !current_page ||
470 (current_page < board_pages_num); ++current_page) {
471 size_t idx = 0;
472 size_t match_pos = 0;
473 size_t after_match_pos = 0;
474 int nret = 0;
475 PCRE2_SIZE var_name_len = 0;
476 size_t challenge_id = rand() % conf->challenges_num;
477 size_t first_thread_idx = current_page *
478 conf->boards[board_idx].
479 threads_per_page;
480 size_t num_threads_this_page =
481 conf->boards[board_idx].threads_per_page;
483 if (first_thread_idx + num_threads_this_page >=
484 thread_ids_num) {
485 num_threads_this_page = thread_ids_num -
486 first_thread_idx;
489 if (current_page) {
490 sprintf(path, "%s/%s/%zu", conf->static_www_folder,
491 conf->boards[board_idx].name, current_page);
492 errno = 0;
494 /* Make the directory. We shouldn't need mkdir -p. */
495 if (mkdir(path, 0755) < 0) {
496 if (errno != EEXIST) {
497 PERROR_MESSAGE("mkdir");
498 ERROR_MESSAGE("mkdir(\"%s\") failed",
499 path);
500 goto done;
504 /* Now open the file */
505 sprintf(path, "%s/%s/%zu/index.html",
506 conf->static_www_folder,
507 conf->boards[board_idx].name,
508 current_page);
509 } else {
510 sprintf(path, "%s/%s/index.html",
511 conf->static_www_folder,
512 conf->boards[board_idx].name);
515 if (f) {
516 if (fclose(f)) {
517 PERROR_MESSAGE("fclose");
518 goto done;
522 if (!(f = fopen(path, "w"))) {
523 PERROR_MESSAGE("fopen");
524 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", path);
525 goto done;
528 find_next_bit:
530 /* Are we done? */
531 if (idx >= board_page_len) {
532 goto success;
535 /* Where does ${FOO} appear next? */
536 nret = pcre2_match(variables, (PCRE2_SPTR) board_page,
537 board_page_len, idx, 0, match_data, 0);
539 if (nret == PCRE2_ERROR_NOMATCH) {
540 CHECK_FPRINTF(f, "%s", board_page + idx);
541 goto success;
544 if (nret < 0) {
545 PCRE2_UCHAR8 err_buf[120];
547 pcre2_get_error_message(nret, err_buf, 120);
548 ERROR_MESSAGE(
549 "pcre2_match: error while matching \"%.*s\": %s"
550 " (PCRE2 %d)", (int) (board_page_len - idx),
551 board_page + idx, err_buf, nret);
552 goto done;
555 /* What is FOO? */
556 pcre2_substring_free(var_name);
557 var_name = 0;
558 var_name_len = 0;
560 if ((nret = pcre2_substring_get_byname(match_data,
561 (PCRE2_SPTR) "var",
562 &var_name,
563 &var_name_len))) {
564 PCRE2_UCHAR8 err_buf[120];
566 pcre2_get_error_message(nret, err_buf, 120);
567 ERROR_MESSAGE(
568 "pcre2_substring_get_byname: %s (PCRE2 %d)",
569 err_buf,
570 nret);
571 goto done;
574 match_pos = pcre2_get_ovector_pointer(match_data)[0];
575 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
576 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), board_page +
577 idx);
578 idx = match_pos;
580 if (!strncasecmp((const char *) var_name, "BOARD",
581 var_name_len)) {
582 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].name);
583 } else if (!strncasecmp((const char *) var_name, "BOARD_TITLE",
584 var_name_len)) {
585 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].title);
586 } else if (!strncasecmp((const char *) var_name, "CHALLENGE",
587 var_name_len)) {
588 CHECK_FPRINTF(f, "%s",
589 conf->challenges[challenge_id].question);
590 } else if (!strncasecmp((const char *) var_name, "CHALLENGE_ID",
591 var_name_len)) {
592 CHECK_FPRINTF(f, "%zu", challenge_id);
593 } else if (!strncasecmp((const char *) var_name, "PAGELINKS",
594 var_name_len)) {
595 CHECK_FPRINTF(f, "[<a href=\"/%s\">0</a>] ",
596 conf->boards[board_idx].name);
598 for (size_t j = 1; j < board_pages_num; ++j) {
599 CHECK_FPRINTF(f,
600 "[<a href=\"/%s/%zu\">%zu</a>] ",
601 conf->boards[board_idx].name, j,
604 } else if (!strncasecmp((const char *) var_name,
605 "RANDOM_HEADER", var_name_len)) {
606 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
607 conf->headers_num]);
608 } else if (!strncasecmp((const char *) var_name, "THREADS",
609 var_name_len)) {
611 * We have to transfer control to db-ZZZ
612 * for a bit - it comes back through
613 * wt_write_post().
615 db_writeback_thread_summaries(board_idx, thread_ids +
616 first_thread_idx,
617 num_threads_this_page, f);
618 } else {
619 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len,
620 var_name);
623 idx = after_match_pos;
624 goto find_next_bit;
625 success:
629 ret = 0;
630 done:
631 pcre2_match_data_free(match_data);
632 pcre2_substring_free(var_name);
634 if (f) {
635 if (fclose(f)) {
636 PERROR_MESSAGE("fclose");
637 ret = -1;
641 free(path);
643 return ret;
647 * Write p out to f as the first post of a thread.
649 * This function is intended to be called by
650 * db_writeback_posts_in_thread(), which is called from wt_write_thread().
652 * Preconditions:
654 * - setup_write_thread() has been called more recently than
655 * clean_write_thread().
657 * - p is a fully filled stored_post (i.e. from a row in the database).
659 * - f is an open, writable FILE *.
661 * - board_idx represents a baord.
663 * - If is_summary is not zero, then board_name is a string like
664 * "m" or "sf". Furthermore, if is_op is also not zero, then
665 * total_posts_in_thread is a number suitable for describing the
666 * thread.
668 * Postconditions (success):
670 * - the contents of p have been written to f, which returned no
671 * errors. The format is not precisely defined.
674 wt_write_post(struct prepared_post *p, FILE *f, uint_fast8_t is_op, uint_fast8_t
675 is_summary, uint_fast8_t is_recent, const char *board_name,
676 uintmax_t in_thread,
677 uintmax_t total_posts_in_thread, uint_fast8_t hr_before)
679 int ret = -1;
680 char *human_readable_time = util_iso8601_from_time_t(p->now);
682 if (!human_readable_time) {
683 ERROR_MESSAGE("util_iso_8601_from_time(%ju) failed",
684 (uintmax_t) p->now);
687 if (hr_before) {
688 /* XXX: make sure this looks decent, both in ff and elinks */
689 CHECK_FPRINTF(f, "<hr />");
692 if (is_op) {
693 CHECK_FPRINTF(f, "<div class=\"op-post\" id=\"post%ju\">",
694 p->id);
695 } else {
696 CHECK_FPRINTF(f, "<div class=\"reply-container\">");
698 if (is_recent) {
699 CHECK_FPRINTF(f, "<div class=\"full-link\">");
700 CHECK_FPRINTF(f, "<a class=\"a-subtle\" href=\"");
701 CHECK_FPRINTF(f, "/%s/res/%ju/#post%ju\">/%s/</a> ",
702 board_name, in_thread, p->id, board_name);
703 CHECK_FPRINTF(f, "</div>");
704 } else {
705 CHECK_FPRINTF(f,
706 "<div class=\"reply-spacer\">..</div>");
709 CHECK_FPRINTF(f, "<div class=\"reply-post\" id=\"post%ju\">",
710 p->id);
713 CHECK_FPRINTF(f, "<span class=\"post-info\">");
715 if (p->subject_len) {
716 CHECK_FPRINTF(f, "<span class=\"post-subject\">%.*s</span> ",
717 (int) p->subject_len, p->subject);
720 if (p->name_len) {
721 if (p->email_len) {
722 CHECK_FPRINTF(f, "<a href=\"mailto:%.*s\">",
723 (int) p->email_len, p->email);
726 CHECK_FPRINTF(f, "<span class=\"post-author\">%.*s</span>",
727 (int) p->name_len, p->name);
729 if (p->email_len) {
730 CHECK_FPRINTF(f, "</a>");
733 if (p->tripcode_len) {
734 CHECK_FPRINTF(f, "<span class=\"post-tripcode\">");
735 CHECK_FPRINTF(f, "!%.*s</span>", (int) p->tripcode_len,
736 p->tripcode);
740 CHECK_FPRINTF(f, " <span class=\"post-time\">%s</span>",
741 human_readable_time ? human_readable_time : "NEVER");
742 CHECK_FPRINTF(f, " <span class=\"post-number\">No. %ju</span>", p->id);
744 if (is_op) {
745 if (p->thread_closed) {
746 CHECK_FPRINTF(f, " <span class=\"thread-closed\">");
747 CHECK_FPRINTF(f, "Closed</span>");
750 if (p->thread_stickied) {
751 CHECK_FPRINTF(f, " <span class=\"thread-stickied\">");
752 CHECK_FPRINTF(f, "Sticky</span>");
756 if (is_summary &&
757 is_op) {
758 CHECK_FPRINTF(f, " [<a href=\"/%s/res/%ju\">View</a>]",
759 board_name, p->id);
762 CHECK_FPRINTF(f, "</span><br />");
764 if (p->system_full_path_len) {
765 CHECK_FPRINTF(f, "<span class=\"file-info\">");
766 CHECK_FPRINTF(f, "<a class=\"filelink\" href=\"%.*s\">",
767 (int) p->system_full_path_len,
768 p->system_full_path);
769 CHECK_FPRINTF(f, "%.*s", (int) p->file_name_len, p->file_name);
770 CHECK_FPRINTF(f, "</a>");
771 CHECK_FPRINTF(f, " (%.*s)</span><br />", (int) p->file_info_len,
772 p->file_info);
773 CHECK_FPRINTF(f, "<div class=\"post-image\">");
774 CHECK_FPRINTF(f, "<a class=\"filelink\" href=\"%.*s\">",
775 (int) p->system_full_path_len,
776 p->system_full_path);
777 CHECK_FPRINTF(f, "<img src=\"%.*s\" alt=\"image\" ",
778 (int) p->system_thumb_path_len,
779 p->system_thumb_path);
780 CHECK_FPRINTF(f, "class=\"thumb\"/>");
781 CHECK_FPRINTF(f, "</a></div>");
784 CHECK_FPRINTF(f, "<div class=\"post-comment\">");
786 if (p->comment_len) {
787 CHECK_FPRINTF(f, "%.*s", (int) p->comment_len, p->comment);
788 } else {
789 CHECK_FPRINTF(f, "&nbsp;");
792 CHECK_FPRINTF(f, "</div>");
793 CHECK_FPRINTF(f, "</div>");
795 if (!is_op) {
796 CHECK_FPRINTF(f, "</div>");
799 if (is_summary &&
800 is_op) {
801 CHECK_FPRINTF(f, "<div class=\"reply-count-info\">");
802 CHECK_FPRINTF(f, "%ju post%s in thread. ",
803 total_posts_in_thread, (total_posts_in_thread ==
804 1) ? "" : "s");
805 CHECK_FPRINTF(f, "<a href=\"/%s/res/%ju\">View thread</a>",
806 board_name, p->id);
807 CHECK_FPRINTF(f, "</div>");
810 ret = 0;
811 done:
812 free(human_readable_time);
814 return ret;
818 * Write out the HTML for the /recent/ page
820 * Preconditions:
822 * - setup_write_thread() has been called more recently than
823 * clean_write_thread().
825 * - for the recent page, THE LOCK IS HELD.
827 * Postconditions (success):
829 * - A file at ${static_www_folder}/recent/index.html has been
830 * written out, representing the last few posts on the site.
833 wt_write_recent_page(void)
835 int ret = -1;
836 size_t idx = 0;
837 size_t match_pos = 0;
838 size_t after_match_pos = 0;
839 pcre2_match_data *match_data = 0;
840 int nret = 0;
841 char *path = 0;
842 size_t len = 0;
843 FILE *f = 0;
844 PCRE2_UCHAR *var_name = 0;
845 PCRE2_SIZE var_name_len = 0;
847 len = snprintf(0, 0, "%s/recent/index.html", conf->static_www_folder);
849 if (len + 1 < len) {
850 ERROR_MESSAGE("overflow");
851 goto done;
854 if (!(path = malloc(len + 1))) {
855 PERROR_MESSAGE("malloc");
856 goto done;
859 sprintf(path, "%s/recent", conf->static_www_folder);
861 /* Make the directory. We shouldn't need mkdir -p. */
862 if (mkdir(path, 0755) < 0) {
863 if (errno != EEXIST) {
864 PERROR_MESSAGE("mkdir");
865 ERROR_MESSAGE("mkdir(\"%s\") failed", path);
866 goto done;
870 sprintf(path, "%s/recent/index.html", conf->static_www_folder);
872 if (!(f = fopen(path, "w"))) {
873 PERROR_MESSAGE("fopen");
874 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", path);
875 goto done;
878 /* 3 capture groups: more than sufficient for ${foo} */
879 if (!(match_data = pcre2_match_data_create(3, 0))) {
880 PERROR_MESSAGE("pcre2_match_data_create");
881 goto done;
884 find_next_bit:
886 /* Are we done? */
887 if (idx >= recent_page_len) {
888 goto success;
891 /* Where does ${FOO} appear next? */
892 nret = pcre2_match(variables, (PCRE2_SPTR) recent_page, recent_page_len,
893 idx, 0, match_data, 0);
895 if (nret == PCRE2_ERROR_NOMATCH) {
896 CHECK_FPRINTF(f, "%s", recent_page + idx);
897 goto success;
900 if (nret < 0) {
901 PCRE2_UCHAR8 err_buf[120];
903 pcre2_get_error_message(nret, err_buf, 120);
904 ERROR_MESSAGE("pcre2_match: error while matching \"%.*s\": %s"
905 " (PCRE2 %d)", (int) (recent_page_len - idx),
906 recent_page + idx,
907 err_buf, nret);
908 goto done;
911 /* What is FOO? */
912 pcre2_substring_free(var_name);
913 var_name = 0;
914 var_name_len = 0;
916 if ((nret = pcre2_substring_get_byname(match_data, (PCRE2_SPTR) "var",
917 &var_name, &var_name_len))) {
918 PCRE2_UCHAR8 err_buf[120];
920 pcre2_get_error_message(nret, err_buf, 120);
921 ERROR_MESSAGE("pcre2_substring_get_byname: %s (PCRE2 %d)",
922 err_buf, nret);
923 goto done;
926 match_pos = pcre2_get_ovector_pointer(match_data)[0];
927 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
928 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), recent_page + idx);
929 idx = match_pos;
931 if (!strncasecmp((const char *) var_name, "RANDOM_HEADER",
932 var_name_len)) {
933 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
934 conf->headers_num]);
935 } else if (!strncasecmp((const char *) var_name, "RECENT_POSTS",
936 var_name_len)) {
938 * We have to transfer control to db-ZZZ for a bit
939 * - it comes back through wt_write_post().
941 db_writeback_recent_posts(f, wt_write_post);
942 } else {
943 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len, var_name);
946 idx = after_match_pos;
947 goto find_next_bit;
948 success:
949 ret = 0;
950 done:
951 pcre2_match_data_free(match_data);
952 pcre2_substring_free(var_name);
954 if (f) {
955 if (fclose(f)) {
956 PERROR_MESSAGE("fclose");
957 ret = -1;
961 free(path);
963 return ret;
967 * Write out the HTML file for a thread
969 * Preconditions:
971 * - setup_write_thread() has been called more recently than
972 * clean_write_thread().
974 * - board_idx represents a board, AND THE LOCK IS HELD.
976 * - thread is the post_id of a thread.
978 * Postconditions (success):
980 * - A file at ${static_www_folder}/${board}/res/${thread}/index.html
981 * has been written out, representing the recent state of the
982 * thread.
985 wt_write_thread(size_t board_idx, uintmax_t thread)
987 int ret = -1;
988 size_t idx = 0;
989 size_t match_pos = 0;
990 size_t after_match_pos = 0;
991 pcre2_match_data *match_data = 0;
992 int nret = 0;
993 FILE *f = 0;
994 size_t len = 0;
995 char *index_path = 0;
996 PCRE2_UCHAR *var_name = 0;
997 PCRE2_SIZE var_name_len = 0;
998 size_t challenge_id = rand() % conf->challenges_num;
1000 len = snprintf(0, 0, "%s/%s/res/%ju/index.html",
1001 conf->static_www_folder, conf->boards[board_idx].name,
1002 thread);
1004 if (len + 1 < len) {
1005 ERROR_MESSAGE("overflow");
1006 goto done;
1009 if (!(index_path = malloc(len + 1))) {
1010 PERROR_MESSAGE("malloc");
1011 goto done;
1014 sprintf(index_path, "%s/%s/res/%ju", conf->static_www_folder,
1015 conf->boards[board_idx].name, thread);
1016 errno = 0;
1018 /* Make the directory. We shouldn't need mkdir -p. */
1019 if (mkdir(index_path, 0755) < 0) {
1020 if (errno != EEXIST) {
1021 PERROR_MESSAGE("mkdir");
1022 ERROR_MESSAGE("mkdir(\"%s\") failed", index_path);
1023 goto done;
1027 /* Now open the file */
1028 sprintf(index_path, "%s/%s/res/%ju/index.html", conf->static_www_folder,
1029 conf->boards[board_idx].name, thread);
1031 if (!(f = fopen(index_path, "w"))) {
1032 PERROR_MESSAGE("fopen");
1033 ERROR_MESSAGE("fopen(\"%s\", \"w\") failed", index_path);
1034 goto done;
1037 /* 3 capture groups: more than sufficient for ${foo} */
1038 if (!(match_data = pcre2_match_data_create(3, 0))) {
1039 PERROR_MESSAGE("pcre2_match_data_create");
1040 goto done;
1043 find_next_bit:
1045 /* Are we done? */
1046 if (idx >= thread_page_len) {
1047 goto success;
1050 /* Where does ${FOO} appear next? */
1051 nret = pcre2_match(variables, (PCRE2_SPTR) thread_page, thread_page_len,
1052 idx, 0, match_data, 0);
1054 if (nret == PCRE2_ERROR_NOMATCH) {
1055 CHECK_FPRINTF(f, "%s", thread_page + idx);
1056 goto success;
1059 if (nret < 0) {
1060 PCRE2_UCHAR8 err_buf[120];
1062 pcre2_get_error_message(nret, err_buf, 120);
1063 ERROR_MESSAGE("pcre2_match: error while matching \"%.*s\": %s"
1064 " (PCRE2 %d)", (int) (thread_page_len - idx),
1065 thread_page + idx,
1066 err_buf, nret);
1067 goto done;
1070 /* What is FOO? */
1071 pcre2_substring_free(var_name);
1072 var_name = 0;
1073 var_name_len = 0;
1075 if ((nret = pcre2_substring_get_byname(match_data, (PCRE2_SPTR) "var",
1076 &var_name, &var_name_len))) {
1077 PCRE2_UCHAR8 err_buf[120];
1079 pcre2_get_error_message(nret, err_buf, 120);
1080 ERROR_MESSAGE("pcre2_substring_get_byname: %s (PCRE2 %d)",
1081 err_buf, nret);
1082 goto done;
1085 match_pos = pcre2_get_ovector_pointer(match_data)[0];
1086 after_match_pos = pcre2_get_ovector_pointer(match_data)[1];
1087 CHECK_FPRINTF(f, "%.*s", (int) (match_pos - idx), thread_page + idx);
1088 idx = match_pos;
1090 if (!strncasecmp((const char *) var_name, "BOARD", var_name_len)) {
1091 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].name);
1092 } else if (!strncasecmp((const char *) var_name, "BOARD_TITLE",
1093 var_name_len)) {
1094 CHECK_FPRINTF(f, "%s", conf->boards[board_idx].title);
1095 } else if (!strncasecmp((const char *) var_name, "CHALLENGE",
1096 var_name_len)) {
1097 CHECK_FPRINTF(f, "%s", conf->challenges[challenge_id].question);
1098 } else if (!strncasecmp((const char *) var_name, "CHALLENGE_ID",
1099 var_name_len)) {
1100 CHECK_FPRINTF(f, "%zu", challenge_id);
1101 } else if (!strncasecmp((const char *) var_name, "OP_SUBJECT",
1102 var_name_len)) {
1103 char *subject = 0;
1104 size_t subject_len = 0;
1106 if (db_extract_subject(board_idx, thread, &subject,
1107 &subject_len) < 0) {
1108 goto done;
1111 if (subject) {
1112 CHECK_FPRINTF(f, "%.*s", (int) subject_len, subject);
1115 free(subject);
1116 } else if (!strncasecmp((const char *) var_name, "RANDOM_HEADER",
1117 var_name_len)) {
1118 CHECK_FPRINTF(f, "%s", conf->headers[rand() %
1119 conf->headers_num]);
1120 } else if (!strncasecmp((const char *) var_name, "THREAD_NUMBER",
1121 var_name_len)) {
1122 CHECK_FPRINTF(f, "%ju", thread);
1123 } else if (!strncasecmp((const char *) var_name, "POSTS",
1124 var_name_len)) {
1126 * We have to transfer control to db-ZZZ for a bit
1127 * - it comes back through wt_write_post().
1129 db_writeback_posts_in_thread(board_idx, thread, f,
1130 wt_write_post);
1131 } else {
1132 CHECK_FPRINTF(f, "${%.*s}", (int) var_name_len, var_name);
1135 idx = after_match_pos;
1136 goto find_next_bit;
1137 success:
1138 ret = 0;
1139 done:
1140 pcre2_match_data_free(match_data);
1141 pcre2_substring_free(var_name);
1143 if (f) {
1144 if (fclose(f)) {
1145 PERROR_MESSAGE("fclose");
1146 ret = -1;
1150 free(index_path);
1152 return ret;
1156 * Clean any memory from this file
1158 * Postconditions (success):
1160 * - Valgrind won't report any memory leaks from this file.
1162 * - setup_write_thread() can be safely called again.
1164 void
1165 clean_write_thread(void)
1167 conf = 0;
1168 free(board_page);
1169 free(thread_page);
1170 board_page = 0;
1171 board_page_len = 0;
1172 thread_page = 0;
1173 thread_page_len = 0;
1174 pcre2_code_free(variables);
1175 variables = 0;