fix: some cases where favicon are broken

This commit is contained in:
Sonny
2023-11-12 01:44:30 +01:00
parent 57905d1485
commit c35eb67890
5 changed files with 88 additions and 109 deletions

2
.gitignore vendored
View File

@@ -18,6 +18,8 @@
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
.idea
.vscode
# debug # debug
npm-debug.log* npm-debug.log*

View File

@@ -1,25 +1,29 @@
const { i18n } = require("./next-i18next.config"); const { i18n } = require("./next-i18next.config");
/** @type {import('next').NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
i18n, i18n,
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ["@svgr/webpack"], use: ["@svgr/webpack"]
}); });
return config; return config;
}, },
images: { images: {
domains: ["localhost", "t3.gstatic.com", "lh3.googleusercontent.com"], remotePatterns: [
formats: ["image/webp"], { hostname: "localhost" },
{ hostname: "t3.gstatic.com" },
{ hostname: "lh3.googleusercontent.com" }
],
formats: ["image/webp"]
}, },
reactStrictMode: false, reactStrictMode: false,
experimental: { experimental: {
webpackBuildWorker: true, webpackBuildWorker: true
}, },
output: "standalone", output: "standalone"
}; };
module.exports = config; module.exports = config;

View File

@@ -12,10 +12,11 @@ interface LinkFaviconProps {
size?: number; size?: number;
noMargin?: boolean; noMargin?: boolean;
} }
export default function LinkFavicon({ export default function LinkFavicon({
url, url,
size = 32, size = 32,
noMargin = false, noMargin = false
}: LinkFaviconProps) { }: LinkFaviconProps) {
const [isFailed, setFailed] = useState<boolean>(false); const [isFailed, setFailed] = useState<boolean>(false);
const [isLoading, setLoading] = useState<boolean>(true); const [isLoading, setLoading] = useState<boolean>(true);
@@ -37,7 +38,7 @@ export default function LinkFavicon({
setFallbackFavicon(); setFallbackFavicon();
handleStopLoading(); handleStopLoading();
}} }}
onLoadingComplete={handleStopLoading} onLoad={handleStopLoading}
height={size} height={size}
width={size} width={size}
alt="icon" alt="icon"

View File

@@ -2,7 +2,14 @@ import { NextApiRequest, NextApiResponse } from "next";
import { parse } from "node-html-parser"; import { parse } from "node-html-parser";
const USER_AGENT = const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54"; "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0";
interface Favicon {
buffer: Buffer;
url: string;
type: string;
size: number;
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@@ -10,71 +17,52 @@ export default async function handler(
) { ) {
const urlParam = (req.query?.url as string) || ""; const urlParam = (req.query?.url as string) || "";
if (!urlParam) { if (!urlParam) {
throw new Error("URL's missing"); throw new Error("URL is missing");
} }
const urlRequest = urlParam + "/favicon.ico"; const { origin, href } = new URL(urlParam);
const faviconRequestUrl = origin + "/favicon.ico";
try { try {
const { favicon, type, size } = await downloadImageFromUrl( return sendImage(res, await getFavicon(faviconRequestUrl));
urlRequest + "/favicon.ico"
);
console.log("[Favicon]", `[first: ${urlRequest}]`, "request");
if (size === 0) {
throw new Error("Empty favicon");
}
if (!isImage(type)) {
throw new Error("Favicon path does not return an image");
}
return sendImage({
content: favicon,
res,
type,
size,
});
} catch (error) { } catch (error) {
const errorMessage = error?.message || "Unable to retrieve favicon"; const errorMessage =
console.error("[Favicon]", `[first: ${urlRequest}]`, errorMessage); error?.message || "Unable to retrieve favicon from favicon.ico url";
console.error("[Favicon]", `[first: ${faviconRequestUrl}]`, errorMessage);
} }
const requestDocument = await makeRequest(href);
const documentAsText = await requestDocument.text();
try { try {
console.log("[Favicon]", `[second: ${urlRequest}]`, "new request"); const faviconPath = findFaviconPath(documentAsText);
const requestDocument = await makeRequest(urlRequest);
const text = await requestDocument.text();
const faviconPath = findFaviconPath(text);
if (!faviconPath) { if (!faviconPath) {
throw new Error("Unable to find favicon path"); throw new Error("Unable to find favicon path");
} }
if (isBase64Image(faviconPath)) { if (isBase64Image(faviconPath)) {
console.log("base64, convert it to buffer"); console.log("[Favicon]", `[first: ${faviconRequestUrl}]`, "base64, convert it to buffer");
const buffer = convertBase64ToBuffer(faviconPath); const buffer = convertBase64ToBuffer(faviconPath);
return sendImage({ return sendImage(res, {
content: buffer, buffer,
res,
type: "image/x-icon", type: "image/x-icon",
size: buffer.length, size: buffer.length,
url: faviconPath
}); });
} }
const pathWithoutFile = popLastSegment(requestDocument.url); const { href } = new URL(requestDocument.url);
const finalUrl = buildFaviconUrl(faviconPath, pathWithoutFile); const finalUrl = href + faviconPath;
const { favicon, type, size } = await downloadImageFromUrl(finalUrl); const favicon = await downloadImageFromUrl(finalUrl);
if (!isImage(type)) { if (!isImage(favicon.type)) {
throw new Error("Favicon path does not return an image"); throw new Error("Favicon path does not return an image");
} }
return sendImage({ return sendImage(res, favicon);
content: favicon,
res,
type,
size,
});
} catch (error) { } catch (error) {
const errorMessage = error?.message || "Unable to retrieve favicon"; const errorMessage = error?.message || "Unable to retrieve favicon";
console.log("[Favicon]", `[second: ${urlRequest}]`, errorMessage); console.log("[Favicon]", `[second: ${faviconRequestUrl}]`, errorMessage);
res.status(404).send({ error: errorMessage }); res.status(404).send({ error: errorMessage, text: documentAsText });
} }
} }
@@ -82,92 +70,76 @@ async function makeRequest(url: string) {
const headers = new Headers(); const headers = new Headers();
headers.set("User-Agent", USER_AGENT); headers.set("User-Agent", USER_AGENT);
const request = await fetch(url, { headers, redirect: "follow" }); return await fetch(url, { headers, redirect: "follow" });
return request;
} }
async function downloadImageFromUrl(url: string): Promise<{ async function downloadImageFromUrl(url: string): Promise<Favicon> {
favicon: Buffer;
url: string;
type: string;
size: number;
}> {
const request = await makeRequest(url); const request = await makeRequest(url);
const blob = await request.blob(); if (!request.ok) {
throw new Error("Request failed");
}
const blob = await request.blob();
return { return {
favicon: Buffer.from(await blob.arrayBuffer()), buffer: Buffer.from(await blob.arrayBuffer()),
url: request.url, url: request.url,
type: blob.type, type: blob.type,
size: blob.size, size: blob.size
}; };
} }
function sendImage({ function sendImage(res: NextApiResponse, {
content, buffer,
res,
type, type,
size, size
}: { }: Favicon) {
content: Buffer;
res: NextApiResponse;
type: string;
size: number;
}) {
res.setHeader("Content-Type", type); res.setHeader("Content-Type", type);
res.setHeader("Content-Length", size); res.setHeader("Content-Length", size);
res.send(content); res.send(buffer);
} }
const selectors = [ const rels = [
"link[rel='icon']", "icon",
"link[rel='shortcut icon']", "shortcut icon",
"link[rel='apple-touch-icon']", "apple-touch-icon",
"link[rel='apple-touch-icon-precomposed']", "apple-touch-icon-precomposed",
"link[rel='apple-touch-startup-image']", "apple-touch-startup-image",
"link[rel='mask-icon']", "mask-icon",
"link[rel='fluid-icon']", "fluid-icon"
]; ];
function findFaviconPath(text) { function findFaviconPath(text: string) {
const document = parse(text); const document = parse(text);
const links = document.querySelectorAll(selectors.join(", ")); const favicon = Array.from(document.getElementsByTagName("link")).find(
const link = links.find( (element) => rels.includes(element.getAttribute("rel")) && element.getAttribute("href"));
(link) => !link.getAttribute("href").startsWith("data:image/")
); if (!favicon) {
if (!link || !link.getAttribute("href")) {
throw new Error("No link/href attribute found"); throw new Error("No link/href attribute found");
} }
return link.getAttribute("href"); return favicon.getAttribute("href");
}
function popLastSegment(url = "") {
const { href } = new URL(url);
const pathWithoutFile = href.split("/");
pathWithoutFile.pop();
return pathWithoutFile.join("/") || "";
}
function buildFaviconUrl(faviconPath, pathWithoutFile) {
if (faviconPath.startsWith("http")) {
return faviconPath;
} else if (faviconPath.startsWith("/")) {
return pathWithoutFile + faviconPath;
} else {
return pathWithoutFile + "/" + faviconPath;
}
} }
function isImage(type: string) { function isImage(type: string) {
return type.includes("image"); return type.includes("image");
} }
function isBase64Image(data) { function isBase64Image(data: string) {
return data.startsWith("data:image/"); return data.startsWith("data:image/");
} }
function convertBase64ToBuffer(base64 = ""): Buffer { function convertBase64ToBuffer(base64: string): Buffer {
const buffer = Buffer.from(base64, "base64"); return Buffer.from(base64, "base64");
return buffer; }
async function getFavicon(url: string): Promise<Favicon> {
if (!url) throw new Error("Missing URL");
const { origin } = new URL(url);
const favicon = await downloadImageFromUrl(url);
if (!isImage(favicon.type) || favicon.size === 0) {
throw new Error("Favicon path does not return an image");
}
return favicon;
} }

View File

@@ -15,10 +15,10 @@ import { useMediaQuery } from "hooks/useMediaQuery";
import useModal from "hooks/useModal"; import useModal from "hooks/useModal";
import { getServerSideTranslation } from "i18n"; import { getServerSideTranslation } from "i18n";
import getUserCategories from "lib/category/getUserCategories"; import getUserCategories from "lib/category/getUserCategories";
import { useTranslation } from "next-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { useTranslation } from "react-i18next";
import { Category, Link, SearchItem } from "types"; import { Category, Link, SearchItem } from "types";
import { withAuthentication } from "utils/session"; import { withAuthentication } from "utils/session";