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 isNcue = host === "ncue.net" || host.endsWith(".ncue.net");
|
||||||
const parts = uu.pathname.split("/").filter(Boolean);
|
const parts = uu.pathname.split("/").filter(Boolean);
|
||||||
const rootFav = `${uu.origin}/favicon.ico`;
|
const rootFav = `${uu.origin}/favicon.ico`;
|
||||||
const hintPath = host === "mail.ncue.net" ? "/roundcube/favicon.ico" : "";
|
const candidates = [];
|
||||||
const hintFav = hintPath ? `${uu.origin}${hintPath}` : "";
|
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 pathFav = isNcue && parts.length ? `${uu.origin}/${parts[0]}/favicon.ico` : "";
|
||||||
const primary = pathFav || hintFav || rootFav;
|
const list = [];
|
||||||
const fallback = primary !== rootFav ? rootFav : "";
|
if (pathFav) list.push(pathFav);
|
||||||
return { primary, fallback };
|
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 {
|
} catch {
|
||||||
return { primary: "", fallback: "" };
|
return { primary: "", fallbackList: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function buildOpenUrl(rawUrl) {
|
function buildOpenUrl(rawUrl) {
|
||||||
@@ -558,13 +579,13 @@
|
|||||||
const copyAttrs = accessible ? "" : ` disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다."`;
|
const copyAttrs = accessible ? "" : ` disabled aria-disabled="true" title="이 링크는 현재 권한으로 접근할 수 없습니다."`;
|
||||||
const fav = faviconCandidates(link.url);
|
const fav = faviconCandidates(link.url);
|
||||||
const faviconHtml = fav && fav.primary
|
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>`;
|
: `<div class="letter">${letter}</div>`;
|
||||||
return `
|
return `
|
||||||
<article class="card${accessible ? "" : " disabled"}" data-id="${esc(link.id)}" data-access="${accessible ? "1" : "0"}">
|
<article class="card${accessible ? "" : " disabled"}" data-id="${esc(link.id)}" data-access="${accessible ? "1" : "0"}">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div class="card-title">
|
<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-wrap">
|
||||||
<div class="title" title="${t}">${t}</div>
|
<div class="title" title="${t}">${t}</div>
|
||||||
<div class="domain" title="${d}">${d}</div>
|
<div class="domain" title="${d}">${d}</div>
|
||||||
@@ -594,6 +615,32 @@
|
|||||||
.filter((l) => matches(l, q))
|
.filter((l) => matches(l, q))
|
||||||
.sort(compare);
|
.sort(compare);
|
||||||
if (el.grid) el.grid.innerHTML = filtered.map(cardHtml).join("");
|
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.empty) el.empty.hidden = filtered.length !== 0;
|
||||||
if (el.meta) {
|
if (el.meta) {
|
||||||
const favCount = all.filter((l) => l.favorite).length;
|
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 parts = u.pathname.split("/").filter(Boolean);
|
||||||
const rootFav = `${u.origin}/favicon.ico`;
|
const rootFav = `${u.origin}/favicon.ico`;
|
||||||
|
|
||||||
// Host-specific hint (Roundcube etc.)
|
// Host-specific hints (Roundcube etc.)
|
||||||
const hintPath = host === "mail.ncue.net" ? "/roundcube/favicon.ico" : "";
|
const candidates = [];
|
||||||
const hintFav = hintPath ? `${u.origin}${hintPath}` : "";
|
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)
|
// Path-based favicon like https://ncue.net/dsyoon/favicon.ico (internal only)
|
||||||
const pathFav = isNcue && parts.length ? `${u.origin}/${parts[0]}/favicon.ico` : "";
|
const pathFav = isNcue && parts.length ? `${u.origin}/${parts[0]}/favicon.ico` : "";
|
||||||
|
|
||||||
const primary = pathFav || hintFav || rootFav;
|
const list = [];
|
||||||
const fallback = primary !== rootFav ? rootFav : "";
|
if (pathFav) list.push(pathFav);
|
||||||
return { primary, fallback };
|
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 {
|
} 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);
|
.sort(compareLinks);
|
||||||
|
|
||||||
el.grid.innerHTML = filtered.map(cardHtml).join("");
|
el.grid.innerHTML = filtered.map(cardHtml).join("");
|
||||||
|
wireFaviconFallbacks();
|
||||||
el.empty.hidden = filtered.length !== 0;
|
el.empty.hidden = filtered.length !== 0;
|
||||||
|
|
||||||
const favCount = all.filter((l) => l.favorite).length;
|
const favCount = all.filter((l) => l.favorite).length;
|
||||||
@@ -355,10 +413,10 @@
|
|||||||
}">
|
}">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<div class="favicon" aria-hidden="true">
|
<div class="favicon" aria-hidden="true" data-letter="${letter}">
|
||||||
${
|
${
|
||||||
fav && fav.primary
|
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 class="letter">${letter}</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user