ssnail.c (16740B)
1 /* 2 * Copyright (c) 2020 Paco Esteban <paco@e1e0.net> 3 * 4 * Permission to use, copy, modify, and distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 #include <sys/queue.h> 18 #include <sys/stat.h> 19 #include <sys/syslimits.h> 20 #include <sys/types.h> 21 22 #include <assert.h> 23 #include <dirent.h> 24 #include <err.h> 25 #include <errno.h> 26 #include <stdio.h> 27 #include <stdlib.h> 28 #include <string.h> 29 #include <time.h> 30 #include <unistd.h> 31 #include <lowdown.h> 32 33 #include "helpers.h" 34 #include "ssnail_error.h" 35 36 #define VERSION "1.3" 37 38 struct article { 39 size_t htmlz; 40 size_t origz; 41 42 char *src_path; 43 char *dst_path; 44 char *orig_content; 45 char *html_content; 46 char *title; 47 char *author; 48 char *date; 49 char *type; 50 51 long long src_mtime; 52 long long dst_mtime; 53 54 LIST_ENTRY(article) entries; 55 }; 56 57 LIST_HEAD(listhead, article) head; 58 59 __dead static void usage(void); 60 static void free_article(struct article *); 61 static void free_articleq(struct listhead *); 62 static void add_index_entry(char **, struct article *); 63 static void add_rss_entry(char **, struct article *, char *); 64 65 struct article *init_article(void); 66 static struct article *populate_article_entry(char *, char *); 67 static struct article *generate_index(char *, char *, char *); 68 69 static const struct ssnail_error *process_dir(char *, char *, int); 70 static const struct ssnail_error *write_html(struct article *, char *, char *); 71 static const struct ssnail_error *sort_articleq(struct listhead *); 72 static const struct ssnail_error *gen_html(struct article *); 73 static const struct ssnail_error *write_rss(char *, char *, char *, char *); 74 75 __dead static void 76 usage(void) 77 { 78 fprintf(stderr, "usage: %s [-F] [-f footer] [-h header] [-i] [-r] [-v] " 79 "-t <title> -u <url> src_folder dst_folder\n", getprogname()); 80 exit(1); 81 } 82 83 int 84 main(int argc, char *argv[]) 85 { 86 struct article *ap = NULL, *ip = NULL; 87 const struct ssnail_error *error = NULL; 88 89 int ch, index = 0, rss = 0, force = 0; 90 char *header_tpl = NULL, *footer_tpl = NULL, 91 *index_listing = NULL, *rss_listing = NULL, 92 *title = NULL, *url = NULL; 93 94 header_tpl = strdup(HEADER); 95 if (header_tpl == NULL) { 96 error = ssnail_error_from_errno("header_tpl def"); 97 goto done; 98 } 99 footer_tpl = strdup(FOOTER); 100 if (footer_tpl == NULL) { 101 error = ssnail_error_from_errno("footer_tpl def"); 102 goto done; 103 } 104 index_listing = strdup(""); 105 if (index_listing == NULL) { 106 error = ssnail_error_from_errno("index_listing def"); 107 goto done; 108 } 109 rss_listing = strdup(""); 110 if (rss_listing == NULL) { 111 error = ssnail_error_from_errno("rss_listing def"); 112 goto done; 113 } 114 115 while ((ch = getopt(argc, argv, "Ff:h:irt:u:v")) != -1) { 116 switch (ch) { 117 case 'F': 118 force = 1; 119 break; 120 case 'f': 121 free(footer_tpl); 122 footer_tpl = strdup(optarg); 123 if (footer_tpl == NULL) { 124 error = ssnail_error_from_errno("footer_tpl"); 125 goto done; 126 } 127 break; 128 case 'h': 129 free(header_tpl); 130 header_tpl = strdup(optarg); 131 if (header_tpl == NULL) { 132 error = ssnail_error_from_errno("footer_tpl"); 133 goto done; 134 } 135 break; 136 case 'i': 137 index = 1; 138 break; 139 case 'r': 140 rss = 1; 141 break; 142 case 't': 143 title = strdup(optarg); 144 if (title == NULL) { 145 error = ssnail_error_from_errno("title"); 146 goto done; 147 } 148 break; 149 case 'u': 150 url = strdup(optarg); 151 if (url == NULL) { 152 error = ssnail_error_from_errno("url"); 153 goto done; 154 } 155 break; 156 case 'v': 157 printf("%s v%s\n", getprogname(), VERSION); 158 return EXIT_SUCCESS; 159 default: 160 usage(); 161 } 162 } 163 argc -= optind; 164 argv += optind; 165 166 LIST_INIT(&head); /* init the linked list */ 167 168 /* title and usage are mandatory */ 169 if (!title || !url) 170 usage(); 171 /* src and dst dir are mandatory. */ 172 if (argc < 2) 173 usage(); 174 175 if ((error = process_dir(argv[0], argv[1], force)) != NULL) 176 goto done; 177 178 if ((error = sort_articleq(&head)) != NULL) 179 goto done; 180 181 printf("Generate html files ... \n"); 182 LIST_FOREACH(ap, &head, entries) { 183 if ((ap->src_mtime > ap->dst_mtime) || force) { 184 printf("... %s\n", ap->dst_path); 185 error = write_html(ap, header_tpl, footer_tpl); 186 if (error) 187 goto done; 188 index = 1; 189 rss = 1; 190 } 191 if (strcmp(ap->type, "article") == 0) { 192 add_index_entry(&index_listing, ap); 193 add_rss_entry(&rss_listing, ap, url); 194 } 195 } 196 197 if (index || force) { 198 printf("Generate index ... \n"); 199 ip = generate_index(argv[1], index_listing, title); 200 if (ip == NULL) { 201 error = ssnail_error_msg(2, "index"); 202 goto done; 203 } 204 error = write_html(ip, header_tpl, footer_tpl); 205 if (error) 206 goto done; 207 } 208 209 if (rss || force) { 210 printf("Generate rss ... \n"); 211 error = write_rss(argv[1], rss_listing, title, url); 212 } 213 214 done: 215 free(title); 216 free(url); 217 free(header_tpl); 218 free(footer_tpl); 219 free(index_listing); 220 free(rss_listing); 221 free_article(ip); 222 free_articleq(&head); 223 224 if (error) { 225 ssnail_error_fprintf(error); 226 return error->code; 227 } 228 229 return EXIT_SUCCESS; 230 } 231 232 static void 233 free_articleq(struct listhead *h) 234 { 235 struct article *a; 236 237 while (!LIST_EMPTY(h)) { 238 a = LIST_FIRST(h); 239 LIST_REMOVE(a, entries); 240 free_article(a); 241 } 242 } 243 244 static void 245 free_article(struct article *a) 246 { 247 if (a) { 248 free(a->src_path); 249 free(a->dst_path); 250 free(a->orig_content); 251 free(a->html_content); 252 free(a->title); 253 free(a->author); 254 free(a->date); 255 free(a->type); 256 free(a); 257 } 258 } 259 260 static const struct ssnail_error * 261 gen_html(struct article *a) 262 { 263 struct lowdown_opts opts; 264 struct lowdown_metaq mq; 265 struct lowdown_meta *md; 266 const struct ssnail_error *error = NULL; 267 268 TAILQ_INIT(&mq); /* create the queue(linked list) of lowdown_meta */ 269 memset(&opts, 0, sizeof(struct lowdown_opts)); /* init opts to 0 */ 270 271 opts.maxdepth = 128; 272 opts.type = LOWDOWN_HTML; 273 opts.feat = LOWDOWN_FOOTNOTES | 274 LOWDOWN_AUTOLINK | 275 LOWDOWN_TABLES | 276 LOWDOWN_SUPER | 277 LOWDOWN_STRIKE | 278 LOWDOWN_FENCED | 279 LOWDOWN_MATH | 280 LOWDOWN_METADATA; 281 opts.oflags = LOWDOWN_HTML_HEAD_IDS | 282 LOWDOWN_HTML_NUM_ENT | 283 LOWDOWN_HTML_OWASP | 284 LOWDOWN_HTML_SKIP_HTML; 285 286 /* use strlen(orig) here because the of the \0 char that 287 * was showing up on the dest buffer. 288 * playing with origz did not work here. 289 */ 290 lowdown_buf(&opts, a->orig_content, strlen(a->orig_content), 291 &a->html_content, &a->htmlz, &mq); 292 293 /* setup default type if not set */ 294 if ((a->type = strdup("page")) == NULL) { 295 error = ssnail_error_from_errno("def type"); 296 goto out; 297 } 298 299 /* collect metadata from markdown */ 300 TAILQ_FOREACH(md, &mq, entries) { 301 if (strcmp(md->key, "title") == 0 ) { 302 if ((a->title = strdup(md->value)) == NULL) { 303 error = ssnail_error_from_errno("title"); 304 goto out; 305 } 306 } 307 if (strcmp(md->key, "author") == 0 ) { 308 if ((a->author = strdup(md->value)) == NULL) { 309 error = ssnail_error_from_errno("author"); 310 goto out; 311 } 312 } 313 if (strcmp(md->key, "date") == 0 ) { 314 if ((a->date = strdup(md->value)) == NULL) { 315 error = ssnail_error_from_errno("date"); 316 goto out; 317 } 318 } 319 if (strcmp(md->key, "type") == 0 ) { 320 free(a->type); 321 if ((a->type = strdup(md->value)) == NULL) { 322 error = ssnail_error_from_errno("type"); 323 goto out; 324 } 325 } 326 } 327 328 out: 329 lowdown_metaq_free(&mq); 330 331 return error; 332 } 333 334 static const struct ssnail_error * 335 write_html(struct article *ap, char *head_tpl, char *foot_tpl) 336 { 337 const struct ssnail_error *error = NULL; 338 FILE *fout; 339 char *header = NULL, *footer = NULL; 340 int c = 0; 341 342 if (ap->title == NULL || ap->author == NULL || ap->date == NULL) { 343 error = ssnail_error_msg(2, "metadata"); 344 goto out; 345 } 346 347 if (load_from_file(&header, head_tpl) == 0) { 348 error = ssnail_error_from_errno("load header"); 349 goto out; 350 } 351 c += str_rep(&header, "$title$", ap->title); 352 c += str_rep(&header, "$author$", ap->author); 353 c += str_rep(&header, "$date$", ap->date); 354 if (c != 0) { 355 error = ssnail_error_from_errno("str_rep"); 356 goto out; 357 } 358 if (load_from_file(&footer, foot_tpl) == 0) { 359 error = ssnail_error_from_errno("load footer"); 360 goto out; 361 } 362 c = str_rep(&footer, "$title$", ap->title); 363 if (c != 0) { 364 error = ssnail_error_from_errno("str_rep"); 365 goto out; 366 } 367 368 if ((fout = fopen(ap->dst_path, "w")) == NULL) { 369 error = ssnail_error_from_errno("openw dst_path"); 370 goto out; 371 } 372 if (fwrite(header, 1, strlen(header), fout) == 0) { 373 error = ssnail_error_from_errno("fwrite header"); 374 goto out; 375 } 376 if (fwrite(ap->html_content, 1, ap->htmlz, fout) == 0) { 377 error = ssnail_error_from_errno("fwrite html content"); 378 goto out; 379 } 380 if (fwrite(footer, 1, strlen(footer), fout) == 0) { 381 error = ssnail_error_from_errno("fwrite footer"); 382 goto out; 383 } 384 if (fclose(fout) != 0) 385 error = ssnail_error_from_errno("close fout"); 386 387 out: 388 free(header); 389 free(footer); 390 return error; 391 } 392 393 static struct article * 394 populate_article_entry(char *src_path, char *dst_path) 395 { 396 struct article *ap = init_article(); 397 struct stat st; 398 int c = 0; 399 400 if (ap == NULL) 401 return NULL; 402 403 ap->src_path = strndup(src_path, PATH_MAX); 404 if (ap->src_path == NULL) 405 goto error; 406 407 ap->dst_path = strndup(dst_path, PATH_MAX); 408 if (ap->dst_path == NULL) 409 goto error; 410 c = str_rep(&(ap->dst_path), ".md", ".html"); 411 if (c == -1) 412 goto error; 413 414 if (stat(ap->src_path, &st) == -1) 415 goto error; 416 ap->src_mtime = st.st_mtim.tv_sec; 417 418 if (stat(ap->dst_path, &st) == -1) { /* dst file does not have to exist */ 419 if (errno != ENOENT) 420 goto error; 421 ap->dst_mtime = 0; 422 } else { 423 ap->dst_mtime = st.st_mtim.tv_sec; 424 } 425 426 if ((ap->origz = load_from_file(&ap->orig_content, ap->src_path)) == 0) 427 goto error; 428 429 if (gen_html(ap) != 0) 430 goto error; 431 432 return ap; 433 error: 434 free_article(ap); 435 return NULL; 436 } 437 438 static const struct ssnail_error * 439 process_dir(char *src, char *dst, int force) 440 { 441 DIR *dirp; 442 struct dirent *dp; 443 struct stat st; 444 struct article *a = NULL; 445 const struct ssnail_error *error = NULL; 446 447 char *src_path = NULL, *dst_path = NULL; 448 449 /* if dst folder does not exist, create it */ 450 if (stat(dst, &st) == -1 && errno == ENOENT) { 451 if (mkdir(dst, 0755) == -1) 452 return ssnail_error_from_errno("mkdir dst"); 453 } 454 455 if ((dirp = opendir(src)) == NULL) 456 return ssnail_error_from_errno("opendir src"); 457 458 while ((dp = readdir(dirp)) != NULL) { 459 if (strcmp(dp->d_name, ".") == 0 460 || strcmp(dp->d_name, "..") == 0) 461 continue; 462 463 if ((src_path = build_full_path(src, dp->d_name)) == NULL) { 464 error = ssnail_error_msg(2, "buildpath src"); 465 goto out; 466 } 467 if ((dst_path = build_full_path(dst, dp->d_name)) == NULL) { 468 error = ssnail_error_msg(2, "buildpath src"); 469 goto out; 470 } 471 472 if (dp->d_type == DT_REG) { 473 if (strcmp(get_filename_ext(src_path), "md") == 0) { 474 a = populate_article_entry(src_path, dst_path); 475 if (a == NULL) { 476 error = ssnail_error_from_errno("populate"); 477 goto out; 478 } 479 LIST_INSERT_HEAD(&head, a, entries); 480 } else { 481 error = copy_file(src_path, dst_path, force); 482 if (error) 483 goto out; 484 } 485 } 486 if (dp->d_type == DT_DIR) 487 error = process_dir(src_path, dst_path, force); 488 489 free(src_path); 490 free(dst_path); 491 } 492 493 out: 494 closedir(dirp); 495 return error; 496 } 497 498 const struct ssnail_error * 499 sort_articleq(struct listhead *h) 500 { 501 struct article *a1 = NULL, *a2 = NULL; 502 int count_elems = 0; 503 504 assert(h != NULL); 505 506 if (LIST_EMPTY(h)) 507 return ssnail_error_msg(2, "sort empty list"); 508 509 /* count list elemets */ 510 LIST_FOREACH(a1, h, entries) { 511 count_elems++; 512 } 513 514 for (int i = 0; i < count_elems; i++) { 515 a1 = LIST_FIRST(h); 516 517 /* move element to the "right" until is the smallest value */ 518 /* or we're at the end of the list */ 519 while ((a2 = LIST_NEXT(a1, entries)) != NULL) { 520 if (!a1->date || !a2->date) 521 return ssnail_error_msg(2, "cmp date NULL"); 522 if (strcmp(a1->date, a2->date) < 0) { 523 LIST_REMOVE(a2, entries); 524 LIST_INSERT_BEFORE(a1, a2, entries); 525 } else 526 a1 = a2; 527 } 528 } 529 530 return NULL; 531 } 532 533 static struct article * 534 generate_index(char *dst_path, char *index_listing, char *title) { 535 assert(index_listing != NULL); 536 537 struct article *a = init_article(); 538 struct tm *timeinfo; 539 time_t now; 540 541 if ((a->dst_path = build_full_path(dst_path, "index.html")) == NULL) 542 goto error; 543 544 a->author = malloc(LOGIN_NAME_MAX); 545 if ((getlogin_r(a->author, LOGIN_NAME_MAX)) != 0) 546 goto error; 547 548 time(&now); 549 timeinfo = localtime(&now); 550 a->date = malloc(SHORT_DATE_Z); 551 if (strftime(a->date, SHORT_DATE_Z, "%Y-%m-%d", timeinfo) == 0) 552 goto error; 553 554 if ((a->title = strdup(title)) == NULL) 555 goto error; 556 if ((a->type = strdup("index")) == NULL) 557 goto error; 558 int ret = asprintf(&a->html_content, "<ul>\n%s</ul>\n", index_listing); 559 if (ret == -1) 560 goto error; 561 a->htmlz = strlen(a->html_content); 562 563 return a; 564 565 error: 566 free_article(a); 567 return NULL; 568 } 569 570 static void 571 add_index_entry(char **index_listing, struct article *a) 572 { 573 char *tmp = NULL; 574 575 if (!index_listing || !a) 576 return; 577 578 const char *entry_format = 579 "%s<li><a href=\"%s\" title=\"%s\">%s</a></li>\n"; 580 char *href = strrchr(a->dst_path, '/'); 581 582 if (href) { 583 /* keep the old listing address so we don't leak */ 584 tmp = *index_listing; 585 asprintf(index_listing, entry_format, *index_listing, 586 href, a->date, a->title); 587 free(tmp); 588 } 589 } 590 591 struct article * 592 init_article(void) 593 { 594 struct article *a = malloc(sizeof(struct article)); 595 596 if (a) { 597 a->src_mtime = 0; 598 a->dst_mtime = 0; 599 a->htmlz = 0; 600 a->origz = 0; 601 a->src_path = NULL; 602 a->dst_path = NULL; 603 a->orig_content = NULL; 604 a->html_content = NULL; 605 a->title = NULL; 606 a->author = NULL; 607 a->date = NULL; 608 a->type = NULL; 609 } 610 611 return a; 612 } 613 614 static void 615 add_rss_entry(char **rss_listing, struct article *a, char *url) 616 { 617 if (!rss_listing || !a) 618 return; 619 620 struct tm timeinfo; 621 char *content = NULL, *html_file = NULL, 622 *link = NULL, pub_date[LONG_DATE_Z], *tmp = NULL; 623 const char *entry_format = 624 "%s<item>\n" 625 "<guid>%s</guid>\n" 626 "<link>%s</link>\n" 627 "<author>%s</author>\n" 628 "<title>%s</title>\n" 629 "<pubDate>%s</pubDate>\n" 630 "<description><![CDATA[\n%s\n]]></description>\n" 631 "</item>\n"; 632 633 if (strptime(a->date, "%Y-%m-%d", &timeinfo) == NULL) 634 goto out; 635 636 /* 637 * we don't keep the hour and tz values on the markdown metadata, so set 638 * it to 00h GMT. Which will never be too off from reality. 639 */ 640 if (strftime(pub_date, LONG_DATE_Z, "%a, %d %b %Y 00:00:00 GMT", &timeinfo) == 0) 641 goto out; 642 643 content = strndup(a->html_content, a->htmlz); 644 html_file = strrchr(a->dst_path, '/') + 1; 645 if (html_file) 646 link = build_full_path(url, html_file); 647 648 if (link && content) { 649 /* keep the old listing address so we don't leak */ 650 tmp = *rss_listing; 651 asprintf(rss_listing, entry_format, *rss_listing, 652 link, link, a->author, a->title, pub_date, content); 653 free(tmp); 654 } 655 656 out: 657 free(link); 658 free(content); 659 } 660 661 static const struct ssnail_error * 662 write_rss(char *dst_path, char *rss_listing, char *title, char *url) 663 { 664 const struct ssnail_error *error = NULL; 665 struct tm *timeinfo; 666 time_t now; 667 FILE *fout; 668 char *rss_path = NULL, *pub_date = NULL, 669 *rss_url = NULL; 670 char *rss_format = 671 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 672 "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n" 673 "<channel>\n" 674 "<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />\n" 675 "<title>%s</title>\n" 676 "<link>%s</link>\n" 677 "<lastBuildDate>%s</lastBuildDate>\n" 678 "<description>%s</description>\n" 679 "%s\n" 680 "</channel>\n" 681 "</rss>\n"; 682 683 time(&now); 684 timeinfo = localtime(&now); 685 pub_date = malloc(LONG_DATE_Z); 686 if (strftime(pub_date, LONG_DATE_Z, "%a, %d %b %Y %H:%M:%S %z", timeinfo) == 0) 687 goto out; 688 689 if ((rss_path = build_full_path(dst_path, "rss.xml")) == NULL) 690 goto out; 691 692 if ((rss_url = build_full_path(url, "rss.xml")) == NULL) 693 goto out; 694 695 if ((fout = fopen(rss_path, "w")) == NULL) { 696 error = ssnail_error_from_errno("openw rss"); 697 goto out; 698 } 699 700 int ret = fprintf(fout, rss_format, rss_url, title, url, pub_date, 701 title, rss_listing); 702 if (ret < 0) { 703 error = ssnail_error_from_errno("write rss"); 704 goto out; 705 } 706 707 if (fclose(fout) != 0) 708 error = ssnail_error_from_errno("close fout"); 709 710 out: 711 free(pub_date); 712 free(rss_path); 713 free(rss_url); 714 return error; 715 }