/* Copyright (C) 2020 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "server.h" bool htmlHideHost; const char *htmlStylesheet = "stylesheet.css"; static enum kcgi_err htmlCSS(struct khtmlreq *html, struct kreq *req) { if (req->fieldmap[Export]) { return 0 || khtml_elem(html, KELEM_STYLE) || khttp_puts(req, CSS) || khtml_closeelem(html, 1); } else { return khtml_attr( html, KELEM_LINK, KATTR_REL, "stylesheet", KATTR_HREF, htmlStylesheet, KATTR__MAX ); } } enum kcgi_err htmlHead(struct khtmlreq *html, struct kreq *req, const char *title) { return khtml_elem(html, KELEM_DOCTYPE) || khtml_attr(html, KELEM_META, KATTR_CHARSET, "utf-8", KATTR__MAX) || khtml_elem(html, KELEM_TITLE) || khtml_puts(html, title) || khtml_closeelem(html, 1) || htmlCSS(html, req) || khtml_elem(html, KELEM_H1) || khtml_puts(html, title) || khtml_closeelem(html, 1); } enum kcgi_err htmlHidden(struct khtmlreq *html, struct kreq *req, enum Key key) { if (!req->fieldmap[key]) return KCGI_OK; return khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "hidden", KATTR_NAME, Keys[key].name, KATTR_VALUE, req->fieldmap[key]->val, KATTR__MAX ); } enum kcgi_err htmlNav(struct khtmlreq *html, struct kreq *req) { if (req->fieldmap[Export]) return KCGI_OK; enum kcgi_err error = 0 || khtml_elem(html, KELEM_NAV) || khtml_elem(html, KELEM_OL) || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, Pages[Networks], KATTR__MAX) || khtml_puts(html, "Networks") || khtml_closeelem(html, 2); if (error) return error; const char *network = NULL; const char *context = NULL; if (req->fieldmap[Network]) { network = req->fieldmap[Network]->parsed.s; if (req->fieldmap[Context]) { context = req->fieldmap[Context]->parsed.s; } } if (network) { char *href = khttp_urlpart( NULL, NULL, Pages[Contexts], Keys[Network].name, network, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); error = 0 || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_puts(html, network) || khtml_closeelem(html, 2); free(href); if (error) return error; } if (context) { const char *context = req->fieldmap[Context]->parsed.s; char *href = khttp_urlpart( NULL, NULL, Pages[Events], Keys[Network].name, network, Keys[Context].name, context, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); error = 0 || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_puts(html, context) || khtml_closeelem(html, 2); free(href); if (error) return error; } const char *query = NULL; if (req->fieldmap[Query]) { query = req->fieldmap[Query]->parsed.s; } return 0 || khtml_closeelem(html, 1) || khtml_attr( html, KELEM_FORM, KATTR_METHOD, "get", KATTR_ACTION, Pages[Search], KATTR__MAX ) || htmlHidden(html, req, Network) || htmlHidden(html, req, Context) || khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "search", KATTR_NAME, Keys[Query].name, KATTR_VALUE, (query ? query : ""), KATTR__MAX ) || khtml_elem(html, KELEM_BUTTON) || khtml_printf( html, "Search %s", (context ? context : network ? network : "") ) || khtml_closeelem(html, 3); } static enum kcgi_err linkify(struct khtmlreq *html, const char *str, size_t len) { static const char *Pattern = "https?://([^[:space:]>\"()]|[(][^)]*[)])+"; static regex_t regex; if (!regex.re_nsub) { int error = regcomp(®ex, Pattern, REG_EXTENDED); assert(!error); } regmatch_t match = {0}; while (!regexec(®ex, str, 1, &match, 0)) { if ((size_t)match.rm_eo > len) break; char *href = strndup(&str[match.rm_so], match.rm_eo - match.rm_so); if (!href) err(EX_OSERR, "strndup"); enum kcgi_err error = 0 || khtml_write(str, match.rm_so, html) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_write(&str[match.rm_so], match.rm_eo - match.rm_so, html) || khtml_closeelem(html, 1); free(href); if (error) return error; str += match.rm_eo; len -= match.rm_eo; } if (len) return khtml_write(str, len, html); return KCGI_OK; } static const struct Style { int fg, bg; bool b, r, i, u; } Default = { .fg = 99, .bg = 99 }; static enum kcgi_err htmlStyle(struct khtmlreq *html, struct Style style) { enum kcgi_err error = KCGI_OK; char class[sizeof("fg99")]; if (style.fg != Default.fg) { snprintf(class, sizeof(class), "fg%02d", style.fg); error = error || khtml_attr( html, KELEM_SPAN, KATTR_CLASS, class, KATTR__MAX ); } if (style.bg != Default.bg) { snprintf(class, sizeof(class), "bg%02d", style.bg); error = error || khtml_attr( html, KELEM_SPAN, KATTR_CLASS, class, KATTR__MAX ); } if (style.b) error = error || khtml_elem(html, KELEM_B); if (style.r) error = error || khtml_elem(html, KELEM_MARK); if (style.i) error = error || khtml_elem(html, KELEM_I); if (style.u) error = error || khtml_elem(html, KELEM_U); return error; } enum kcgi_err htmlIRC(struct khtmlreq *html, const char *str) { enum kcgi_err error = khtml_attr( html, KELEM_SPAN, KATTR_CLASS, "irc", KATTR__MAX ); if (error) return error; size_t top = khtml_elemat(html); struct Style style = Default; for (;;) { size_t len = strcspn(str, "\2\3\17\26\35\37"); if (len) { error = 0 || khtml_closeto(html, top) || htmlStyle(html, style) || linkify(html, str, len); if (error) return error; } str += len; if (!*str) break; switch (*str++) { break; case '\2': style.b ^= true; break; case '\17': style = Default; break; case '\26': style.r ^= true; break; case '\35': style.i ^= true; break; case '\37': style.u ^= true; break; case '\3': { if (!isdigit(*str)) { style.fg = Default.fg; style.bg = Default.bg; break; } style.fg = *str++ - '0'; if (isdigit(*str)) style.fg = style.fg * 10 + *str++ - '0'; if (str[0] != ',' || !isdigit(str[1])) break; str++; style.bg = *str++ - '0'; if (isdigit(*str)) style.bg = style.bg * 10 + *str++ - '0'; } } } return khtml_closeto(html, top) || khtml_closeelem(html, 1); } static const char *timestamp(time_t time) { static char stamp[sizeof("0000-00-00 00:00:00")]; strftime(stamp, sizeof(stamp), "%F %T", gmtime(&time)); return stamp; } static enum kcgi_err eventTime(struct khtmlreq *html, struct kreq *req, struct Event *event) { char *page = NULL; char *href = NULL; if (event->network && event->context && !req->fieldmap[Export]) { page = khttp_urlpart( NULL, NULL, Pages[Events], Keys[Network].name, event->network, Keys[Context].name, event->context, Keys[After].name, timestamp(event->time - eventsOverlap), NULL ); if (!page) err(EX_OSERR, "khttp_urlpart"); } asprintf(&href, "%s#%" PRId64, (page ? page : ""), event->event); if (!href) err(EX_OSERR, "asprintf"); if (page) free(page); const char *stamp = timestamp(event->time); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "time", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_attr( html, KELEM_TIME, KATTR_DATETIME, stamp, KATTR__MAX ) || khtml_puts(html, stamp) || khtml_closeelem(html, 3); free(href); return error; } static enum kcgi_err eventNetwork(struct khtmlreq *html, struct kreq *req, struct Event *event) { if (!req->fieldmap[Query] || req->fieldmap[Network]) return KCGI_OK; if (req->fieldmap[Export]) { return 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "network", KATTR__MAX) || khtml_puts(html, event->network) || khtml_closeelem(html, 1); } char *href = khttp_urlpart( NULL, NULL, Pages[Search], Keys[Network].name, event->network, Keys[Query].name, req->fieldmap[Query]->parsed.s, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "network", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_puts(html, event->network) || khtml_closeelem(html, 2); free(href); return error; } static enum kcgi_err eventContext(struct khtmlreq *html, struct kreq *req, struct Event *event) { if (!req->fieldmap[Query] || req->fieldmap[Context]) return KCGI_OK; if (req->fieldmap[Export]) { return 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "context", KATTR__MAX) || khtml_puts(html, event->context) || khtml_closeelem(html, 1); } char *href = khttp_urlpart( NULL, NULL, Pages[Search], Keys[Network].name, event->network, Keys[Context].name, event->context, Keys[Query].name, req->fieldmap[Query]->parsed.s, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "context", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_puts(html, event->context) || khtml_closeelem(html, 2); free(href); return error; } static int hash(const char *str) { if (*str == '~') str++; uint32_t hash = 0; for (; *str; ++str) { hash = (hash << 5) | (hash >> 27); hash ^= *str; hash *= 0x27220A95; } return 2 + hash % 74; } static enum kcgi_err eventNick(struct khtmlreq *html, const struct Event *event) { char *mask = NULL; char class[sizeof("fg99")]; snprintf( class, sizeof(class), "fg%02d", hash(strcmp(event->user, "*") ? event->user : event->nick) ); asprintf( &mask, "%s!%s@%s", event->nick, event->user, (htmlHideHost ? "*" : event->host) ); if (!mask) err(EX_OSERR, "asprintf"); const char *format = "%s"; switch (event->type) { break; case Privmsg: format = "<%s>"; break; case Notice: format = "-%s-"; break; case Action: format = "* %s"; break; default:; } enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "nick", KATTR__MAX) || khtml_attr( html, KELEM_SPAN, KATTR_CLASS, class, KATTR_TITLE, mask, KATTR__MAX ) || khtml_printf(html, format, event->nick) || khtml_closeelem(html, 2); free(mask); return error; } static enum kcgi_err maybeMessage(struct khtmlreq *html, const struct Event *event) { if (!event->message) return KCGI_OK; return 0 || khtml_puts(html, " (") || htmlIRC(html, event->message) || khtml_puts(html, ")"); } static enum kcgi_err eventMessage(struct khtmlreq *html, const struct Event *event) { enum kcgi_err error = khtml_attr( html, KELEM_TD, KATTR_CLASS, "message", KATTR__MAX ); if (error) return error; switch (event->type) { break; case Join: { error = khtml_puts(html, "joined"); } break; case Part: { error = khtml_puts(html, "left") || maybeMessage(html, event); } break; case Quit: { error = khtml_puts(html, "quit") || maybeMessage(html, event); } break; case Kick: { if (!event->target) break; error = 0 || khtml_printf(html, "kicked %s", event->target) || maybeMessage(html, event); } break; case Nick: { if (!event->target) break; char class[sizeof("fg99")]; snprintf( class, sizeof(class), "fg%02d", hash(strcmp(event->user, "*") ? event->user : event->target) ); error = 0 || khtml_puts(html, "changed nick to ") || khtml_attr(html, KELEM_SPAN, KATTR_CLASS, class, KATTR__MAX) || khtml_puts(html, event->target) || khtml_closeelem(html, 1); } break; case Topic: { if (!event->message) break; error = 0 || khtml_puts(html, "set the topic: ") || htmlIRC(html, event->message); } break; case Ban: { if (!event->target) break; error = khtml_printf(html, "banned %s", event->target); } break; case Unban: { if (!event->target) break; error = khtml_printf(html, "unbanned %s", event->target); } break; default: { if (event->message) error = htmlIRC(html, event->message); } } return error || khtml_closeelem(html, 1); } static const char *Types[TypesLen] = { #define X(id, name) [id] = name, ENUM_TYPE #undef X }; enum kcgi_err htmlEvent(struct khtmlreq *html, struct kreq *req, struct Event *event) { const char *type = (event->type < TypesLen ? Types[event->type] : "unknown"); return 0 || khtml_attrx( html, KELEM_TR, KATTR_ID, KATTRX_INT, event->event, KATTR_CLASS, KATTRX_STRING, type, KATTR__MAX ) || eventTime(html, req, event) || eventNetwork(html, req, event) || eventContext(html, req, event) || eventNick(html, event) || eventMessage(html, event) || khtml_closeelem(html, 1); } static const char *SourceURL = "https://git.causal.agency/scooper"; static const char *SyntaxURL = { "https://www.sqlite.org/fts5.html#full_text_query_syntax" }; static const char *Columns = { "network, channel, query, nick, user, target, message" }; enum kcgi_err htmlFooter(struct khtmlreq *html, struct kreq *req) { if (req->fieldmap[Export]) return KCGI_OK; return 0 || khtml_closeto(html, 0) || khtml_elem(html, KELEM_FOOTER) || khtml_elem(html, KELEM_SPAN) || khtml_attr(html, KELEM_A, KATTR_HREF, SourceURL, KATTR__MAX) || khtml_puts(html, "scooper is AGPLv3+") || khtml_closeelem(html, 2) || khtml_putc(html, ' ') || khtml_elem(html, KELEM_SPAN) || khtml_attr(html, KELEM_A, KATTR_HREF, SyntaxURL, KATTR__MAX) || khtml_puts(html, "Search syntax") || khtml_closeelem(html, 1) || khtml_putc(html, ' ') || khtml_attr(html, KELEM_SPAN, KATTR_TITLE, Columns, KATTR__MAX) || khtml_puts(html, "Columns") || khtml_closeto(html, 0); }