main xs_webmention.h
  1/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */
  2
  3#ifndef _XS_WEBMENTION_H
  4
  5#define _XS_WEBMENTION_H
  6
  7#include "xs.h"
  8#include "xs_regex.h"
  9#include "xs_curl.h"
 10
 11int xs_webmention_send(const char *source, const char *target, const char *user_agent);
 12int xs_webmention_hook(const char *source, const char *target, const char *user_agent);
 13
 14
 15#ifdef XS_IMPLEMENTATION
 16
 17#include "xs_http.h"
 18
 19int xs_webmention_send(const char *source, const char *target, const char *user_agent)
 20/* sends a Webmention to target.
 21   Returns: < 0, error; 0, no Webmention endpoint; > 0, Webmention sent */
 22{
 23    int status = 0;
 24    xs *endpoint = NULL;
 25
 26    xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention");
 27    xs *headers = xs_dict_new();
 28    headers = xs_dict_set(headers, "accept", "text/html");
 29    headers = xs_dict_set(headers, "user-agent", ua);
 30
 31    xs *h_req = NULL;
 32    int p_size = 0;
 33
 34    /* try first a HEAD, to see if there is a Webmention Link header */
 35    h_req = xs_http_request("HEAD", target, headers, NULL, 0, &status, NULL, &p_size, 0);
 36
 37    /* return immediate failures */
 38    if (!xs_http_valid_status(status))
 39        return -1;
 40
 41    const char *link = xs_dict_get(h_req, "link");
 42
 43    if (xs_is_string(link) && xs_regex_match(link, "rel *= *(\"|')?webmention")) {
 44        /* endpoint is between < and > */
 45        xs *r = xs_regex_select_n(link, "<[^>]+>", 1);
 46
 47        if (xs_list_len(r) == 1) {
 48            endpoint = xs_dup(xs_list_get(r, 0));
 49            endpoint = xs_strip_chars_i(endpoint, "<>");
 50        }
 51    }
 52
 53    if (endpoint == NULL) {
 54        /* no Link header; get the content */
 55        xs *g_req = NULL;
 56        xs *payload = NULL;
 57
 58        g_req = xs_http_request("GET", target, headers, NULL, 0, &status, &payload, &p_size, 0);
 59
 60        if (!xs_http_valid_status(status))
 61            return -1;
 62
 63        const char *ctype = xs_dict_get(g_req, "content-type");
 64
 65        /* not HTML? no point in looking inside */
 66        if (!xs_is_string(ctype) || xs_str_in(ctype, "text/html") == -1)
 67            return -2;
 68
 69        if (!xs_is_string(payload))
 70            return -3;
 71
 72        xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>");
 73        const char *link;
 74
 75        xs_list_foreach(links, link) {
 76            if (xs_regex_match(link, "rel *= *(\"|')?webmention")) {
 77                /* found; extract the href */
 78                xs *r = xs_regex_select_n(link, "href *= *(\"|')?[^\"]+(\"|')", 1);
 79
 80                if (xs_list_len(r) == 1) {
 81                    xs *l = xs_split_n(xs_list_get(r, 0), "=", 1);
 82
 83                    if (xs_list_len(l) == 2) {
 84                        endpoint = xs_dup(xs_list_get(l, 1));
 85                        endpoint = xs_strip_chars_i(endpoint, " \"'");
 86
 87                        break;
 88                    }
 89                }
 90            }
 91        }
 92    }
 93
 94    /* is it a relative endpoint? */
 95    if (xs_is_string(endpoint)) {
 96        if (!xs_startswith(endpoint, "https://") && !xs_startswith(endpoint, "http://")) {
 97            xs *l = xs_split(target, "/");
 98
 99            if (xs_list_len(l) < 3)
100                endpoint = xs_free(endpoint);
101            else {
102                xs *s = xs_fmt("%s/" "/%s", xs_list_get(l, 0), xs_list_get(l, 2));
103                endpoint = xs_str_wrap_i(s, endpoint, NULL);
104            }
105        }
106    }
107
108    if (xs_is_string(endpoint)) {
109        /* got it! */
110        headers = xs_dict_set(headers, "content-type", "application/x-www-form-urlencoded");
111
112        xs *body = xs_fmt("source=%s&target=%s", source, target);
113
114        xs *rsp = xs_http_request("POST", endpoint, headers, body, strlen(body), &status, NULL, 0, 0);
115
116        if (!xs_http_valid_status(status))
117            status = -4;
118        else
119            status = 1;
120    }
121    else
122        status = 0;
123
124    return status;
125}
126
127
128int xs_webmention_hook(const char *source, const char *target, const char *user_agent)
129/* a Webmention has been received for a target that is ours; check if the source
130   really contains a link to our target */
131{
132    int status = 0;
133
134    xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention");
135    xs *headers = xs_dict_new();
136    headers = xs_dict_set(headers, "accept", "text/html");
137    headers = xs_dict_set(headers, "user-agent", ua);
138
139    xs *g_req = NULL;
140    xs *payload = NULL;
141    int p_size = 0;
142
143    g_req = xs_http_request("GET", source, headers, NULL, 0, &status, &payload, &p_size, 0);
144
145    if (!xs_http_valid_status(status))
146        return -1;
147
148    if (!xs_is_string(payload))
149        return -2;
150
151    /* note: a "rogue" webmention can include a link to our target in commented-out HTML code */
152
153    xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>");
154    const char *link;
155
156    status = 0;
157    xs_list_foreach(links, link) {
158        /* if the link contains our target, it's valid */
159        if (xs_str_in(link, target) != -1) {
160            status = 1;
161            break;
162        }
163    }
164
165    return status;
166}
167
168
169#endif /* XS_IMPLEMENTATION */
170
171#endif /* _XS_WEBMENTION_H */