esifawafwu

This commit is contained in:
2026-09-28 19:03:31 +02:00
parent 5eb33f97c8
commit dde0a2d271
62 changed files with 3760 additions and 1865 deletions
+77 -11
View File
@@ -369,6 +369,55 @@
return finishData;
}
// ── Add a passkey to the current anonymous identity ───────────────────
async addPasskey() {
if (!this.supported) throw new Error('WebAuthn / Passkeys are not supported in this browser.');
const csrfToken = (window.f0ckSession && window.f0ckSession.csrf_token)
|| document.querySelector('meta[name="csrf-token"]')?.content || '';
const beginRes = await fetch('/api/v2/anon/passkey/add/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
body: JSON.stringify({})
});
const beginData = await beginRes.json();
if (!beginData.success) throw new Error(beginData.msg || 'Server error');
const rawChallenge = beginData.options.challenge;
const opts = beginData.options;
const cred = await navigator.credentials.create({ publicKey: {
rp: opts.rp,
user: {
id: b64urlToArr(opts.user.id),
name: opts.user.name,
displayName: opts.user.displayName
},
challenge: b64urlToArr(rawChallenge),
pubKeyCredParams: opts.pubKeyCredParams,
timeout: opts.timeout || 60000,
excludeCredentials: (opts.excludeCredentials || []).map(c => ({ type: c.type, id: b64urlToArr(c.id) })),
authenticatorSelection: opts.authenticatorSelection,
attestation: opts.attestation || 'none'
}});
const finishRes = await fetch('/api/v2/anon/passkey/add/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
body: JSON.stringify({
challenge: rawChallenge,
credentialId: arrToB64url(cred.rawId),
clientDataJSON: arrToB64url(cred.response.clientDataJSON),
attestationObject: arrToB64url(cred.response.attestationObject),
hw_fingerprint: await getHardwareFingerprint()
})
});
const finishData = await finishRes.json();
if (!finishData.success) throw new Error(finishData.msg || 'Registration failed');
return finishData;
}
// ── Helpers ───────────────────────────────────────────────────────────────
_hasLocalPasskey() {
@@ -490,12 +539,26 @@
const res = await fetch('/api/v2/anon/identity');
const data = await res.json();
const fpEl = document.getElementById('anon-pk-fp-display');
const credEl = document.getElementById('anon-pk-cred-display');
if (fpEl) fpEl.textContent = data.fingerprint ? data.fingerprint.slice(7, 15) : 'none';
if (credEl) credEl.textContent = data.credential_id || '—';
this._renderPasskeyCount(data.passkey_count, data.passkey_max);
} catch (e) {}
}
_renderPasskeyCount(count, max) {
const countEl = document.getElementById('anon-pk-count');
const maxEl = document.getElementById('anon-pk-max');
const addBtn = document.getElementById('anon-pk-add-btn');
if (countEl && typeof count === 'number') countEl.textContent = count;
if (maxEl && typeof max === 'number') maxEl.textContent = max;
if (addBtn && typeof count === 'number' && typeof max === 'number') {
const full = count >= max;
addBtn.disabled = full;
addBtn.innerHTML = full
? '<i class="fa-solid fa-lock"></i> Limit reached'
: '<i class="fa-solid fa-plus"></i> Add passkey';
}
}
// ── Init ──────────────────────────────────────────────────────────────────
async init() {
@@ -568,24 +631,27 @@
const modalOverlay = document.getElementById('anon-passkey-modal');
if (modalOverlay) modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) this.closeModal(); });
// Add-passkey button inside modal (for anonymous users to add a second passkey)
// Add-passkey button inside modal — attaches a new passkey to this identity (max enforced server-side)
const addBtn = document.getElementById('anon-pk-add-btn');
if (addBtn) {
addBtn.addEventListener('click', async () => {
const errEl = document.getElementById('anon-pk-error');
if (errEl) errEl.style.display = 'none';
addBtn.disabled = true;
try {
await this.register();
if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!');
this._refreshModalContent();
const data = await this.addPasskey();
if (typeof window.showToastNotification === 'function') window.showToastNotification('Passkey added');
this._renderPasskeyCount(data.passkey_count, data.passkey_max);
} catch (err) {
console.warn('[ANON_PASSKEY] Modal add passkey error:', err);
if (err && err.name === 'NotAllowedError') {
alert('Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.');
} else {
alert('Failed to add passkey: ' + ((err && err.message) || 'Unknown error'));
if (errEl) {
errEl.style.display = '';
errEl.textContent = (err && err.name === 'NotAllowedError')
? 'Cancelled or blocked. Unlock your password manager and try again.'
: ((err && err.message) || 'Failed to add passkey.');
}
} finally {
addBtn.disabled = false;
this._refreshModalContent();
}
});
}
+39 -14
View File
@@ -1,3 +1,12 @@
// Pages whose content depends on the rating/mime filter and must reload when it changes.
// Profile pages count even when both previews are empty (then no .posts grid is rendered).
// Elements whose horizontal drags belong to them (sliders, sideways-scrolling rows): swipe gestures ignore touches starting here.
window.F0CK_NO_SWIPE_SELECTOR = 'input[type="range"], .f0ck-tuner-subtabs-nav, .f0ck-tuner-mode-bar, [data-no-swipe]';
window.isFilterReloadableView = function () {
return !!document.querySelector('.posts, .tags-grid') || /^\/user\/[^/]+\/?$/.test(window.location.pathname);
};
// Normalize percent-encoded characters in the URL bar that are safe to show decoded.
// Runs immediately so the address bar is clean before any other JS runs.
@@ -1437,7 +1446,7 @@ window.cancelAnimFrame = (function () {
window.flashMessage('ALL MODE ACTIVATED');
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isGridView = window.isFilterReloadableView();
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
let reloadPromise = null;
if (isOnaraOpen) {
@@ -1501,7 +1510,7 @@ window.cancelAnimFrame = (function () {
window.flashMessage(label);
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isGridView = window.isFilterReloadableView();
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
let reloadPromise = null;
if (isOnaraOpen) {
@@ -13173,7 +13182,7 @@ window.cancelAnimFrame = (function () {
// Refresh content
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isGridView = window.isFilterReloadableView();
const isItemView = document.getElementById('prev') || document.getElementById('next');
let reloadPromise = null;
@@ -13789,6 +13798,7 @@ window.cancelAnimFrame = (function () {
}, 50);
const updateDomAfterSave = (resData) => {
const itemPath = (resData && resData.item_path) || itemId;
try {
const finalRating = resData.rating || selectedRating || 'untagged';
@@ -13838,11 +13848,11 @@ window.cancelAnimFrame = (function () {
sep.textContent = '→';
const a = document.createElement('a');
a.href = `/${itemId}`;
a.href = `/${itemPath}`;
a.className = 'location-link is-rehosted';
a.title = `View rehosted post #${itemId} on f0ckm`;
a.title = `View rehosted post /${itemPath} on f0ckm`;
a.style.cssText = 'color: #4ade80; font-weight: 700;';
a.innerHTML = `<i class="fa-solid fa-check"></i> #${itemId}`;
a.innerHTML = `<i class="fa-solid fa-check"></i> /${itemPath}`;
locEl.appendChild(sep);
locEl.appendChild(a);
@@ -13860,8 +13870,8 @@ window.cancelAnimFrame = (function () {
if (rehostBtn) {
rehostBtn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${itemId}
<a href="/${itemPath}" class="chan-rehost-action-btn rehosted" title="View /${itemPath}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> /${itemPath}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i>
@@ -13985,8 +13995,8 @@ window.cancelAnimFrame = (function () {
btn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${data.item_id}
<a href="/${data.item_path || data.item_id}" class="chan-rehost-action-btn rehosted" title="View /${data.item_path || data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> /${data.item_path || data.item_id}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i>
@@ -13997,7 +14007,7 @@ window.cancelAnimFrame = (function () {
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
}
if (typeof window.openChanRehostModal === 'function') {
const newAnchor = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${data.item_id}"]`) || document.querySelector(`.chan-rehost-action-btn[href="/${data.item_id}"]`);
const newAnchor = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${data.item_id}"]`) || document.querySelector(`.chan-rehost-action-btn[href="/${data.item_path || data.item_id}"]`);
window.openChanRehostModal({ itemId: data.item_id, anchorEl: newAnchor });
}
} else {
@@ -16561,6 +16571,19 @@ window.cancelAnimFrame = (function () {
document.addEventListener('touchstart', e => {
if (window.f0ckSession?.disable_swiping) return;
if (document.body.classList.contains('modal-open')) return;
// Sliders and horizontally scrolling rows own their horizontal drags: never start a
// sidebar-drag or item-swipe from them (the swipe's preventDefault would freeze the slider)
if (e.target?.closest?.(window.F0CK_NO_SWIPE_SELECTOR)) {
swipeRT.xDown = null;
swipeRT.yDown = null;
swipeRT.xDiff = 0;
swipeRT.yDiff = 0;
swipeRT.isSidebarCandidate = false;
swipeRT.isDraggingSidebar = false;
swipeRT.startedInSidebarZone = true; // also blocks prev/next navigation on touchend
return;
}
const touch = e.touches[0];
@@ -17474,13 +17497,15 @@ window.cancelAnimFrame = (function () {
if (document.body.classList.contains('modal-open')) return;
if (!isLSActive()) return;
swipeRT.isDraggingLeftSidebar = false;
swipeRT.isLeftSidebarCandidate = false;
if (e.target?.closest?.(window.F0CK_NO_SWIPE_SELECTOR)) return;
const { sb, layout } = getLS();
if (!sb || !layout) return;
const touch = e.touches[0];
const isHidden = layout.classList.contains('sidebar-hidden');
swipeRT.isDraggingLeftSidebar = false;
swipeRT.isLeftSidebarCandidate = false;
if (isHidden) {
if (touch.clientX < 60) {
@@ -20145,7 +20170,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Trigger reload context-aware
const fromModal = !!menu.closest('#excluded-tags-overlay');
const postsEl = document.querySelector('.posts, .tags-grid');
const postsEl = window.isFilterReloadableView();
let reloadPromise = null;
// Set flag so hideAllModals (called inside loadPageAjax) skips the filter overlay
+7 -5
View File
@@ -1455,7 +1455,7 @@
${item.is_external && item.external_board && item.external_tid
? `<a class="scroll-id-link" href="https://boards.4chan.org/${item.external_board}/thread/${item.external_tid}#p${item.external_id}" target="_blank">/${item.external_board}/thread/${item.external_tid}</a>`
: (item.local_id
? `<a class="scroll-id-link" href="/${item.local_id}" target="_blank">#${item.local_id}</a>`
? `<a class="scroll-id-link" href="/${item.local_path || item.local_id}" target="_blank">/${item.local_path || item.local_id}</a>`
: `<a class="scroll-id-link" href="/abyss/${item.id}">#${item.id}</a>`)}
</div>`;
slide.appendChild(meta);
@@ -1486,7 +1486,7 @@
</button>
${item.is_external ? (
item.local_id
? `<a class="scroll-btn rehost-btn success" href="/${item.local_id}" target="_blank" title="${_i.already_added || 'Already added'}">
? `<a class="scroll-btn rehost-btn success" href="/${item.local_path || item.local_id}" target="_blank" title="${_i.already_added || 'Already added'}">
<div class="scroll-btn-icon"><i class="fa-solid fa-check"></i></div>
<span class="scroll-btn-label">${_i.view_label || 'View'}</span>
</a>`
@@ -1761,6 +1761,7 @@
const external_media_url = `https://i.4cdn.org/${data.board}/${p.tim}${ext}`;
const local_id = allRehosts[external_media_url] || null;
const local_path = (local_id && data.rehost_paths && data.rehost_paths[local_id]) || local_id;
return {
id: `${data.board}/${p.no}`,
@@ -1770,6 +1771,7 @@
external_source: '4chan',
is_external: true,
local_id,
local_path,
external_media_url,
mime: isVideo ? 'video/unknown' : (isImage ? 'image/unknown' : 'application/octet-stream'),
dest: `/api/v2/scroller/external/4chan/${data.board}/media/${p.tim}${ext}`,
@@ -1972,7 +1974,7 @@
// Update button to link to the new site-internal post
setTimeout(() => {
btn.outerHTML = `
<a href="/${data.item_id}" target="_blank" class="scroll-btn rehost-btn success" style="text-decoration:none;">
<a href="/${data.item_path || data.item_id}" target="_blank" class="scroll-btn rehost-btn success" style="text-decoration:none;">
<div class="scroll-btn-icon"><i class="fa-solid fa-arrow-up-right-from-square"></i></div>
<span class="scroll-btn-label">View</span>
</a>
@@ -1983,8 +1985,8 @@
if (slide) {
const idLink = slide.querySelector('.scroll-id-link');
if (idLink) {
idLink.href = `/${data.item_id}`;
idLink.textContent = `#${data.item_id}`;
idLink.href = `/${data.item_path || data.item_id}`;
idLink.textContent = `/${data.item_path || data.item_id}`;
}
// Reflect rehoster's username and local timestamp
if (window.scrollerUsername) {
+2 -2
View File
@@ -3401,7 +3401,7 @@ window.initUploadForm = (selector) => {
}
}
if (shouldRedirectToItem) {
if (shouldRedirectToItem && !lastData?.manual_approval) {
const isMultiNonAlbumShitpost = isShitpost && !isAlbum && (uploadedResults.length > 1 || successCount > 1 || totalFilesToUpload > 1);
let targetUrl;
if (isMultiNonAlbumShitpost) {
@@ -3416,7 +3416,7 @@ window.initUploadForm = (selector) => {
window.location.href = targetUrl;
}
}
// else: stay on current page
// else: stay on current page (including manual_approval pending)
}
} else {
restoreBtn();
+5 -2
View File
@@ -60,6 +60,9 @@
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
);
// Anonymous uploaders may change the rating of their own item without full manage rights
const canRate = canManage || !!document.querySelector('#tags[data-can-rate="true"]');
// Only remove existing dynamically generated tags
[...inner.querySelectorAll(".badge")].forEach(tag => {
// Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself
@@ -100,7 +103,7 @@
span.classList.add('rating-tag', `is-${tag.normalized}`);
span.dataset.rating = tag.normalized;
if (activePostId) span.dataset.itemId = activePostId;
if (canManage) {
if (canRate) {
span.classList.add('can-cycle');
}
} else {
@@ -144,7 +147,7 @@
const hasRating = !!lastRatingTag;
if (!hasRating) {
const untaggedSpan = document.createElement("span");
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`;
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canRate ? ' can-cycle' : ''}`;
untaggedSpan.dataset.rating = 'untagged';
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
const lbl = document.createElement("span");