Roundcube 스킨 파비콘 탐색 및 CSP 안전 폴백
- mail.ncue.net에서 Roundcube 스킨 경로 favicon 후보를 순차 탐색 - 이미지 오류 처리 로직을 JS 이벤트로 바꿔 CSP 환경에서도 폴백 동작 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
63
index.html
63
index.html
@@ -523,14 +523,35 @@
|
||||
const isNcue = host === "ncue.net" || host.endsWith(".ncue.net");
|
||||
const parts = uu.pathname.split("/").filter(Boolean);
|
||||
const rootFav = `${uu.origin}/favicon.ico`;
|
||||
const hintPath = host === "mail.ncue.net" ? "/roundcube/favicon.ico" : "";
|
||||
const hintFav = hintPath ? `${uu.origin}${hintPath}` : "";
|
||||
const candidates = [];
|
||||
if (host === "mail.ncue.net") {
|
||||
candidates.push(
|
||||
`${uu.origin}/roundcube/skins/elastic/images/favicon.ico`,
|
||||
`${uu.origin}/roundcube/skins/larry/images/favicon.ico`,
|
||||
`${uu.origin}/roundcube/skins/classic/images/favicon.ico`,
|
||||
`${uu.origin}/skins/elastic/images/favicon.ico`,
|
||||
`${uu.origin}/skins/larry/images/favicon.ico`,
|
||||
`${uu.origin}/skins/classic/images/favicon.ico`,
|
||||
`${uu.origin}/roundcube/favicon.ico`
|
||||
);
|
||||
}
|
||||
const pathFav = isNcue && parts.length ? `${uu.origin}/${parts[0]}/favicon.ico` : "";
|
||||
const primary = pathFav || hintFav || rootFav;
|
||||
const fallback = primary !== rootFav ? rootFav : "";
|
||||
return { primary, fallback };
|
||||
const list = [];
|
||||
if (pathFav) list.push(pathFav);
|
||||
list.push(...candidates);
|
||||
list.push(rootFav);
|
||||
const uniq = [];
|
||||
const seen = new Set();
|
||||
for (const x of list) {
|
||||
const v = String(x || "").trim();
|
||||
if (!v) continue;
|
||||
if (seen.has(v)) continue;
|
||||
seen.add(v);
|
||||
uniq.push(v);
|
||||
}
|
||||
return { primary: uniq[0] || "", fallbackList: uniq.slice(1) };
|
||||
} catch {
|
||||
return { primary: "", fallback: "" };
|
||||
return { primary: "", fallbackList: [] };
|
||||
}
|
||||
}
|
||||
function buildOpenUrl(rawUrl) {
|
||||
@@ -558,13 +579,13 @@
|
||||
const copyAttrs = accessible ? "" : ` disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다."`;
|
||||
const fav = faviconCandidates(link.url);
|
||||
const faviconHtml = fav && fav.primary
|
||||
? `<img src="${esc(fav.primary)}" data-fb="${esc(fav.fallback || "")}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" onerror="const p=this.parentNode; const fb=this.dataset.fb; if(fb){ this.dataset.fb=''; this.onerror=null; this.src=fb; return; } this.remove(); if(p) p.insertAdjacentHTML('beforeend','<div class="letter">${letter}</div>');" />`
|
||||
? `<img src="${esc(fav.primary)}" data-fb="${esc((fav.fallbackList || []).join("|"))}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" />`
|
||||
: `<div class="letter">${letter}</div>`;
|
||||
return `
|
||||
<article class="card${accessible ? "" : " disabled"}" data-id="${esc(link.id)}" data-access="${accessible ? "1" : "0"}">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<div class="favicon" aria-hidden="true">${faviconHtml}</div>
|
||||
<div class="favicon" aria-hidden="true" data-letter="${letter}">${faviconHtml}</div>
|
||||
<div class="title-wrap">
|
||||
<div class="title" title="${t}">${t}</div>
|
||||
<div class="domain" title="${d}">${d}</div>
|
||||
@@ -594,6 +615,32 @@
|
||||
.filter((l) => matches(l, q))
|
||||
.sort(compare);
|
||||
if (el.grid) el.grid.innerHTML = filtered.map(cardHtml).join("");
|
||||
// bind favicon fallback chain (no inline onerror; works under CSP)
|
||||
if (el.grid) {
|
||||
const imgs = el.grid.querySelectorAll("img[data-fb]");
|
||||
for (const img of imgs) {
|
||||
if (img.dataset.bound === "1") continue;
|
||||
img.dataset.bound = "1";
|
||||
img.addEventListener(
|
||||
"error",
|
||||
() => {
|
||||
const fb = String(img.dataset.fb || "");
|
||||
const list = fb ? fb.split("|").filter(Boolean) : [];
|
||||
const next = list.shift();
|
||||
if (next) {
|
||||
img.dataset.fb = list.join("|");
|
||||
img.src = next;
|
||||
return;
|
||||
}
|
||||
const p = img.parentNode;
|
||||
const letter = p && p.getAttribute ? p.getAttribute("data-letter") : "";
|
||||
img.remove();
|
||||
if (p) p.insertAdjacentHTML("beforeend", `<div class="letter">${esc(letter || "L")}</div>`);
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
if (el.empty) el.empty.hidden = filtered.length !== 0;
|
||||
if (el.meta) {
|
||||
const favCount = all.filter((l) => l.favorite).length;
|
||||
|
||||
76
script.js
76
script.js
@@ -189,18 +189,75 @@
|
||||
const parts = u.pathname.split("/").filter(Boolean);
|
||||
const rootFav = `${u.origin}/favicon.ico`;
|
||||
|
||||
// Host-specific hint (Roundcube etc.)
|
||||
const hintPath = host === "mail.ncue.net" ? "/roundcube/favicon.ico" : "";
|
||||
const hintFav = hintPath ? `${u.origin}${hintPath}` : "";
|
||||
// Host-specific hints (Roundcube etc.)
|
||||
const candidates = [];
|
||||
if (host === "mail.ncue.net") {
|
||||
// common Roundcube skin favicon locations (server files)
|
||||
candidates.push(
|
||||
`${u.origin}/roundcube/skins/elastic/images/favicon.ico`,
|
||||
`${u.origin}/roundcube/skins/larry/images/favicon.ico`,
|
||||
`${u.origin}/roundcube/skins/classic/images/favicon.ico`,
|
||||
// sometimes Roundcube is mounted at /
|
||||
`${u.origin}/skins/elastic/images/favicon.ico`,
|
||||
`${u.origin}/skins/larry/images/favicon.ico`,
|
||||
`${u.origin}/skins/classic/images/favicon.ico`,
|
||||
// legacy attempt
|
||||
`${u.origin}/roundcube/favicon.ico`
|
||||
);
|
||||
}
|
||||
|
||||
// Path-based favicon like https://ncue.net/dsyoon/favicon.ico (internal only)
|
||||
const pathFav = isNcue && parts.length ? `${u.origin}/${parts[0]}/favicon.ico` : "";
|
||||
|
||||
const primary = pathFav || hintFav || rootFav;
|
||||
const fallback = primary !== rootFav ? rootFav : "";
|
||||
return { primary, fallback };
|
||||
const list = [];
|
||||
if (pathFav) list.push(pathFav);
|
||||
list.push(...candidates);
|
||||
list.push(rootFav);
|
||||
|
||||
// de-dup + drop empties
|
||||
const uniq = [];
|
||||
const seen = new Set();
|
||||
for (const x of list) {
|
||||
const v = String(x || "").trim();
|
||||
if (!v) continue;
|
||||
if (seen.has(v)) continue;
|
||||
seen.add(v);
|
||||
uniq.push(v);
|
||||
}
|
||||
|
||||
const primary = uniq[0] || "";
|
||||
const rest = uniq.slice(1);
|
||||
return { primary, fallbackList: rest };
|
||||
} catch {
|
||||
return { primary: "", fallback: "" };
|
||||
return { primary: "", fallbackList: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function wireFaviconFallbacks() {
|
||||
const imgs = el.grid ? el.grid.querySelectorAll("img[data-fb]") : [];
|
||||
for (const img of imgs) {
|
||||
if (img.dataset.bound === "1") continue;
|
||||
img.dataset.bound = "1";
|
||||
img.addEventListener(
|
||||
"error",
|
||||
() => {
|
||||
const fb = String(img.dataset.fb || "");
|
||||
const list = fb ? fb.split("|").filter(Boolean) : [];
|
||||
const next = list.shift();
|
||||
if (next) {
|
||||
img.dataset.fb = list.join("|");
|
||||
img.src = next;
|
||||
return;
|
||||
}
|
||||
const p = img.parentNode;
|
||||
const letter = p && p.getAttribute ? p.getAttribute("data-letter") : "";
|
||||
img.remove();
|
||||
if (p) {
|
||||
p.insertAdjacentHTML("beforeend", `<div class="letter">${escapeHtml(letter || "L")}</div>`);
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +367,7 @@
|
||||
.sort(compareLinks);
|
||||
|
||||
el.grid.innerHTML = filtered.map(cardHtml).join("");
|
||||
wireFaviconFallbacks();
|
||||
el.empty.hidden = filtered.length !== 0;
|
||||
|
||||
const favCount = all.filter((l) => l.favorite).length;
|
||||
@@ -355,10 +413,10 @@
|
||||
}">
|
||||
<div class="card-head">
|
||||
<div class="card-title">
|
||||
<div class="favicon" aria-hidden="true">
|
||||
<div class="favicon" aria-hidden="true" data-letter="${letter}">
|
||||
${
|
||||
fav && fav.primary
|
||||
? `<img src="${escapeHtml(fav.primary)}" data-fb="${escapeHtml(fav.fallback || "")}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" onerror="const p=this.parentNode; const fb=this.dataset.fb; if(fb){ this.dataset.fb=''; this.onerror=null; this.src=fb; return; } this.remove(); if(p) p.insertAdjacentHTML('beforeend','<div class="letter">${letter}</div>');" />`
|
||||
? `<img src="${escapeHtml(fav.primary)}" data-fb="${escapeHtml((fav.fallbackList || []).join("|"))}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" />`
|
||||
: `<div class="letter">${letter}</div>`
|
||||
}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user