Ghost/ghost/webmentions/lib/MentionDiscoveryService.js
Daniel Lockyer 67278d91a1 Lazyloaded cheerio dependency
- we can move this lower in the code so it's not eagerly loaded when
  it's not needed
2024-10-09 17:03:04 +01:00

74 lines
2.3 KiB
JavaScript

const logging = require('@tryghost/logging');
module.exports = class MentionDiscoveryService {
#externalRequest;
constructor({externalRequest}) {
this.#externalRequest = externalRequest;
}
/**
* Fetches the given URL to identify the webmention endpoint
* @param {URL} url
* @returns {Promise<URL|null>}
*/
async getEndpoint(url) {
try {
const response = await this.#externalRequest(url.href, {
throwHttpErrors: true,
followRedirect: true,
maxRedirects: 10,
timeout: 15000,
retry: {
// Only retry on network issues, or specific HTTP status codes
limit: 3
}
});
return this.getEndpointFromResponse(response);
} catch (error) {
logging.error(`Error fetching ${url.href} to discover webmention endpoint`, error);
return null;
}
}
/**
* @private
* Parses the given response for the first webmention endpoint
* @param {Object} response
* @returns {Promise<URL|null>}
*/
async getEndpointFromResponse(response) {
let href;
let endpoint;
// Link: <uri-reference>; param1=value1; param2="value2"
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
const linkHeader = response.headers.link;
if (linkHeader && linkHeader.includes('rel="webmention"')) {
linkHeader.split(',').forEach((p) => {
if (p.includes('rel="webmention"')) {
href = p.substring(p.indexOf('<') + 1, p.indexOf('>'));
endpoint = new URL(href);
return;
}
});
}
if (endpoint) {
return endpoint;
}
// must be html to find links/tags
if (!response.headers['content-type'].includes('text/html')) {
return null;
}
const cheerio = require('cheerio');
const $ = cheerio.load(response.body);
// must be first <link> OR <a> element with rel=webmention
href = $('a[rel="webmention"],link[rel="webmention"]').first().attr('href');
endpoint = href ? new URL(href) : null;
return endpoint;
}
};