Files
wehub-resource-sync 3cd11ababe
Check Markdown links / linkChecker (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:38:56 +08:00

166 lines
4.9 KiB
TypeScript

import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { getText } from '../utils';
export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
let error = null;
let verdict = false;
let data: any = null;
try {
let content = getText(context, eventType);
const not = parameters.not || false;
if (!content) {
throw new Error('Missing text to analyze');
}
// Find all URLs in the content, they may or may not start with http(s)
// Regex explanation: https?:\/\/[^\s,"'{}\[\]]+
// https?:\/\/ - matches http or https
// [^\s,"'{}\[\]]+ - matches any characters that are not whitespace, comma, single quote, curly brace, or square bracket
const urls = content.match(/https?:\/\/[^\s,"'{}\[\]]+/g) || [];
const onlyDNS = parameters.onlyDNS || false;
if (urls.length === 0) {
data = {
explanation: 'No URLs found in the text.',
urls: [],
validationMethod: onlyDNS ? 'DNS lookup' : 'HTTP request',
not,
textExcerpt:
content.length > 100 ? content.slice(0, 100) + '...' : content,
};
return { error, verdict: false, data };
}
let validationResults: { url: string; isValid: boolean }[] = [];
if (onlyDNS) {
const results = await Promise.all(
urls.map(async (url) => ({
url,
isValid: await checkDNS(url),
}))
);
validationResults = results;
const allValid = results.every((result) => result.isValid);
verdict = not ? !allValid : allValid;
} else {
const results = await Promise.all(
urls.map(async (url) => ({
url,
isValid: await checkUrl(url),
}))
);
validationResults = results;
const allValid = results.every((result) => result.isValid);
verdict = not ? !allValid : allValid;
}
const invalidUrls = validationResults
.filter((result) => !result.isValid)
.map((result) => result.url);
const validUrls = validationResults
.filter((result) => result.isValid)
.map((result) => result.url);
data = {
verdict,
not,
explanation: verdict
? not
? `All URLs are invalid as expected (${invalidUrls.length} of ${urls.length}).`
: `All URLs are valid (${validUrls.length} found).`
: not
? `Some URLs are valid when they should all be invalid (${validUrls.length} of ${urls.length}).`
: `Some URLs are invalid (${invalidUrls.length} of ${urls.length} failed).`,
validUrls,
invalidUrls,
validationMethod: onlyDNS ? 'DNS lookup' : 'HTTP request',
textExcerpt:
content.length > 100 ? content.slice(0, 100) + '...' : content,
};
} catch (e: any) {
error = e;
const content = getText(context, eventType);
data = {
explanation: `An error occurred while validating URLs: ${e.message}`,
validationMethod: parameters.onlyDNS ? 'DNS lookup' : 'HTTP request',
not: parameters.not || false,
textExcerpt: content
? content.length > 100
? content.slice(0, 100) + '...'
: content
: 'No text available',
};
}
return { error, verdict, data };
};
async function checkUrl(target: string): Promise<boolean> {
const controller = new AbortController();
const { signal } = controller;
let timeoutId: NodeJS.Timeout;
try {
// Create a promise that rejects after timeout
const timeoutPromise = new Promise<Response>((_, reject) => {
timeoutId = setTimeout(() => {
controller.abort();
reject(new Error('Request timeout'));
}, 3000);
});
// Race between the fetch and the timeout
const response = await Promise.race([
fetch(target, {
method: 'HEAD', // Use HEAD instead of GET for efficiency
signal,
headers: {
'User-Agent': 'URLValidator/1.0', // Add user agent to prevent some 403s
},
}),
timeoutPromise,
]);
clearTimeout(timeoutId!);
return response.ok;
} catch (error) {
return false;
} finally {
clearTimeout(timeoutId!);
controller.abort(); // Always abort to clean up
}
}
async function checkDNS(target: string): Promise<boolean> {
try {
const parsedUrl = new URL(target);
const response = await fetch(
// Using DNS over HTTPS (DoH) for cross-runtime compatibility (works in both Edge and Node.js)
// https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/
`https://1.1.1.1/dns-query?name=${parsedUrl.hostname}`,
{
headers: {
accept: 'application/dns-json',
},
}
);
const data: Record<string, any> = await response.json();
return data.Status === 0 && data.Answer && data.Answer.length > 0;
} catch (error) {
return false;
}
}