diff --git a/js/channelProfile.js b/js/channelProfile.js
index 7efbf85f..7e8fd071 100644
--- a/js/channelProfile.js
+++ b/js/channelProfile.js
@@ -15,6 +15,7 @@ import createPopover from "./ui/overlay/popoverEngine.js";
import { VideoCard } from "./ui/components/VideoCard.js";
import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js";
import { ALLOW_NSFW_CONTENT } from "./config.js";
+import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
import {
calculateZapShares,
describeShareType,
@@ -2097,73 +2098,26 @@ function trimProfileString(value) {
return value.trim();
}
-function normalizeProfileMediaUrl(value) {
- const trimmed = trimProfileString(value);
- if (!trimmed) {
- return "";
- }
-
- if (/^data:image\//i.test(trimmed)) {
- return trimmed;
- }
-
- if (/^blob:/i.test(trimmed)) {
- return trimmed;
- }
-
- if (/^https?:\/\//i.test(trimmed)) {
- return trimmed;
- }
-
- if (trimmed.startsWith("//")) {
- return `https:${trimmed}`;
- }
-
- if (/^ipfs:\/\//i.test(trimmed)) {
- const ipfsPath = trimmed
- .replace(/^ipfs:\/\//i, "")
- .replace(/^ipfs\//i, "");
- if (!ipfsPath) {
- return "";
- }
- return `https://ipfs.io/ipfs/${encodeURI(ipfsPath)}`;
- }
-
- if (/^(?:\.\.\/|\.\/|\/)/.test(trimmed)) {
- return trimmed;
- }
-
- if (/^assets\//i.test(trimmed)) {
- return trimmed;
- }
-
- const localhostPattern = /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:[/?#].*)?$/i;
- if (localhostPattern.test(trimmed)) {
- return `http://${trimmed}`;
- }
-
- const domainPattern =
- /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,}(?::\d+)?(?:[/?#].*)?$/i;
- if (domainPattern.test(trimmed)) {
- return `https://${trimmed}`;
- }
-
- return "";
-}
-
function normalizeChannelProfileMetadata(raw = {}) {
const name =
trimProfileString(raw.display_name) ||
trimProfileString(raw.name) ||
"Unknown User";
const picture =
- normalizeProfileMediaUrl(raw.picture) ||
- normalizeProfileMediaUrl(raw.image) ||
+ sanitizeProfileMediaUrl(raw.picture) ||
+ sanitizeProfileMediaUrl(raw.image) ||
FALLBACK_CHANNEL_AVATAR;
const about = trimProfileString(raw.about || raw.bio);
const website = trimProfileString(raw.website || raw.url);
const banner =
- normalizeProfileMediaUrl(raw.banner || raw.header || raw.background) || "";
+ sanitizeProfileMediaUrl(
+ raw.banner ||
+ raw.header ||
+ raw.background ||
+ raw.cover ||
+ raw.cover_image ||
+ raw.coverImage
+ ) || "";
const lud16 = trimProfileString(raw.lud16);
const lud06 = trimProfileString(raw.lud06);
const lightningAddress =
diff --git a/js/state/cache.js b/js/state/cache.js
index 52332ff6..e2a30e0a 100644
--- a/js/state/cache.js
+++ b/js/state/cache.js
@@ -1,4 +1,5 @@
import { userLogger } from "../utils/logger.js";
+import { sanitizeProfileMediaUrl } from "../utils/profileMedia.js";
import {
readUrlHealthFromStorage,
removeUrlHealthFromStorage,
@@ -577,7 +578,9 @@ export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) {
const normalized = {
name: profile.name || profile.display_name || "Unknown",
- picture: profile.picture || "assets/svg/default-profile.svg",
+ picture:
+ sanitizeProfileMediaUrl(profile.picture || profile.image) ||
+ "assets/svg/default-profile.svg",
};
const about = sanitizeProfileString(profile.about || profile.aboutMe);
@@ -590,7 +593,14 @@ export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) {
normalized.website = website;
}
- const banner = sanitizeProfileString(profile.banner || profile.header);
+ const banner = sanitizeProfileMediaUrl(
+ profile.banner ||
+ profile.header ||
+ profile.background ||
+ profile.cover ||
+ profile.cover_image ||
+ profile.coverImage
+ );
if (banner) {
normalized.banner = banner;
}
diff --git a/js/utils/profileMedia.js b/js/utils/profileMedia.js
new file mode 100644
index 00000000..07cb2dfd
--- /dev/null
+++ b/js/utils/profileMedia.js
@@ -0,0 +1,80 @@
+import { devLogger } from "./logger.js";
+
+const LOCALHOST_PATTERN = /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:[/?#].*)?$/i;
+const DOMAIN_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,}(?::\d+)?(?:[/?#].*)?$/i;
+
+function normalizeIpfsGatewayUrl(trimmed) {
+ const ipfsPath = trimmed.replace(/^ipfs:\/\//i, "").replace(/^ipfs\//i, "");
+ if (!ipfsPath) {
+ return "";
+ }
+ return `https://ipfs.io/ipfs/${encodeURI(ipfsPath)}`;
+}
+
+function coerceHttpToHttps(url) {
+ if (!/^http:\/\//i.test(url)) {
+ return url;
+ }
+
+ if (LOCALHOST_PATTERN.test(url.replace(/^http:\/\//i, ""))) {
+ return url;
+ }
+
+ return `https://${url.slice(7)}`;
+}
+
+export function sanitizeProfileMediaUrl(value) {
+ if (typeof value !== "string") {
+ return "";
+ }
+
+ let trimmed = value.trim();
+ if (!trimmed) {
+ return "";
+ }
+
+ trimmed = trimmed.replace(/^['"]+|['"]+$/g, "");
+ if (!trimmed) {
+ return "";
+ }
+
+ if (/^data:image\//i.test(trimmed)) {
+ return trimmed;
+ }
+
+ if (/^blob:/i.test(trimmed)) {
+ return trimmed;
+ }
+
+ if (/^ipfs:\/\//i.test(trimmed)) {
+ return normalizeIpfsGatewayUrl(trimmed);
+ }
+
+ if (trimmed.startsWith("//")) {
+ trimmed = `https:${trimmed}`;
+ }
+
+ if (/^(?:\.\.\/|\.\/|\/)/.test(trimmed)) {
+ return trimmed;
+ }
+
+ if (/^assets\//i.test(trimmed)) {
+ return trimmed;
+ }
+
+ if (!/^https?:\/\//i.test(trimmed)) {
+ if (LOCALHOST_PATTERN.test(trimmed)) {
+ trimmed = `http://${trimmed}`;
+ } else if (DOMAIN_PATTERN.test(trimmed)) {
+ trimmed = `https://${trimmed}`;
+ } else {
+ devLogger?.warn?.(
+ "[profileMedia.sanitizeProfileMediaUrl] Unsupported media URL",
+ trimmed
+ );
+ return "";
+ }
+ }
+
+ return coerceHttpToHttps(trimmed);
+}
diff --git a/views/channel-profile.html b/views/channel-profile.html
index e7e2bafc..ba77fd80 100644
--- a/views/channel-profile.html
+++ b/views/channel-profile.html
@@ -5,8 +5,12 @@
