|
@@ -0,0 +1,118 @@
|
|
|
|
|
+/* copyright (c) 2025 grunfink et al. / MIT license */
|
|
|
|
|
+
|
|
|
|
|
+#ifndef _XS_WEBMENTION_H
|
|
|
|
|
+
|
|
|
|
|
+#define _XS_WEBMENTION_H
|
|
|
|
|
+
|
|
|
|
|
+int xs_webmention_send(const char *source, const char *target, const char *user_agent);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+#ifdef XS_IMPLEMENTATION
|
|
|
|
|
+
|
|
|
|
|
+int xs_webmention_send(const char *source, const char *target, const char *user_agent)
|
|
|
|
|
+/* sends a Webmention to target.
|
|
|
|
|
+ Returns: < 0, error; 0, no Webmention endpoint; > 0, Webmention sent */
|
|
|
|
|
+{
|
|
|
|
|
+ int status = 0;
|
|
|
|
|
+ xs *endpoint = NULL;
|
|
|
|
|
+
|
|
|
|
|
+ xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention");
|
|
|
|
|
+ xs *headers = xs_dict_new();
|
|
|
|
|
+ headers = xs_dict_set(headers, "accept", "text/html");
|
|
|
|
|
+ headers = xs_dict_set(headers, "user-agent", ua);
|
|
|
|
|
+
|
|
|
|
|
+ xs *h_req = NULL;
|
|
|
|
|
+ int p_size = 0;
|
|
|
|
|
+
|
|
|
|
|
+ /* try first a HEAD, to see if there is a Webmention Link header */
|
|
|
|
|
+ h_req = xs_http_request("HEAD", target, headers, NULL, 0, &status, NULL, &p_size, 0);
|
|
|
|
|
+
|
|
|
|
|
+ /* return immediate failures */
|
|
|
|
|
+ if (status < 200 || status > 299)
|
|
|
|
|
+ return -1;
|
|
|
|
|
+
|
|
|
|
|
+ const char *link = xs_dict_get(h_req, "link");
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_is_string(link) && xs_regex_match(link, "rel *= *(\"|')?webmention")) {
|
|
|
|
|
+ /* endpoint is between < and > */
|
|
|
|
|
+ xs *r = xs_regex_select_n(link, "<[^>]+>", 1);
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_list_len(r) == 1) {
|
|
|
|
|
+ endpoint = xs_dup(xs_list_get(r, 0));
|
|
|
|
|
+ endpoint = xs_strip_chars_i(endpoint, "<>");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (endpoint == NULL) {
|
|
|
|
|
+ /* no Link header; get the content */
|
|
|
|
|
+ xs *g_req = NULL;
|
|
|
|
|
+ xs *payload = NULL;
|
|
|
|
|
+
|
|
|
|
|
+ g_req = xs_http_request("GET", target, headers, NULL, 0, &status, &payload, &p_size, 0);
|
|
|
|
|
+
|
|
|
|
|
+ if (status < 200 || status > 299)
|
|
|
|
|
+ return -1;
|
|
|
|
|
+
|
|
|
|
|
+ const char *ctype = xs_dict_get(g_req, "content-type");
|
|
|
|
|
+
|
|
|
|
|
+ /* not HTML? no point in looking inside */
|
|
|
|
|
+ if (!xs_is_string(ctype) || xs_str_in(ctype, "text/html") == -1)
|
|
|
|
|
+ return -2;
|
|
|
|
|
+
|
|
|
|
|
+ if (!xs_is_string(payload))
|
|
|
|
|
+ return -3;
|
|
|
|
|
+
|
|
|
|
|
+ xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>");
|
|
|
|
|
+ const char *link;
|
|
|
|
|
+
|
|
|
|
|
+ xs_list_foreach(links, link) {
|
|
|
|
|
+ if (xs_regex_match(link, "rel *= *(\"|')?webmention")) {
|
|
|
|
|
+ /* found; extract the href */
|
|
|
|
|
+ xs *r = xs_regex_select_n(link, "href *= *(\"|')?[^\"]+(\"|')", 1);
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_list_len(r) == 1) {
|
|
|
|
|
+ xs *l = xs_split_n(xs_list_get(r, 0), "=", 1);
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_list_len(l) == 2) {
|
|
|
|
|
+ endpoint = xs_dup(xs_list_get(l, 1));
|
|
|
|
|
+ endpoint = xs_strip_chars_i(endpoint, " \"'");
|
|
|
|
|
+
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* is it a relative endpoint? */
|
|
|
|
|
+ if (xs_is_string(endpoint)) {
|
|
|
|
|
+ if (!xs_startswith(endpoint, "https://") && !xs_startswith(endpoint, "http://")) {
|
|
|
|
|
+ xs *l = xs_split(target, "/");
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_list_len(l) < 3)
|
|
|
|
|
+ endpoint = xs_free(endpoint);
|
|
|
|
|
+ else {
|
|
|
|
|
+ xs *s = xs_fmt("%s/" "/%s", xs_list_get(l, 0), xs_list_get(l, 2));
|
|
|
|
|
+ endpoint = xs_str_wrap_i(s, endpoint, NULL);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (xs_is_string(endpoint)) {
|
|
|
|
|
+ /* got it! */
|
|
|
|
|
+ headers = xs_dict_set(headers, "content-type", "application/x-www-form-urlencoded");
|
|
|
|
|
+
|
|
|
|
|
+ xs *body = xs_fmt("source=%s&target=%s", source, target);
|
|
|
|
|
+
|
|
|
|
|
+ xs *rsp = xs_http_request("POST", endpoint, headers, body, strlen(body), &status, NULL, 0, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ status = 0;
|
|
|
|
|
+
|
|
|
|
|
+ return status >= 200 && status <= 299;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+#endif /* XS_IMPLEMENTATION */
|
|
|
|
|
+
|
|
|
|
|
+#endif /* _XS_WEBMENTION_H */
|