From aaf8e902a17333f36791dc73fa0352530400fc36 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 20 Apr 2026 21:02:59 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20refactorer=20ShaaritThemeConfig.save()?= =?UTF-8?q?=20pour=20utiliser=20readEditForm()=20unifi=C3=A9=20(GET=20/adm?= =?UTF-8?q?in/shaare=3Fpost=3D)=20qui=20retourne=20token/lfId/existin?= =?UTF-8?q?g=20en=20une=20seule=20requ=C3=AAte,=20=C3=A9liminer=20readExis?= =?UTF-8?q?tingConfig()=20et=20getTokenFrom()=20redondants,=20auto-d=C3=A9?= =?UTF-8?q?tection=20create=20vs=20update=20via=20pr=C3=A9sence=20lf=5Fid?= =?UTF-8?q?=20dans=20form=20Shaarli,=20cleanup=20duplicates=20en=20excluan?= =?UTF-8?q?t=20bookmark=20sauvegard=C3=A9=20(lfId=20ou=20premier=20candida?= =?UTF-8?q?t),=20et=20retourner=20flag=20created=20dans=20r=C3=A9sultat=20?= =?UTF-8?q?save?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shaarli-pro/js/script.js | 76 ++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index 896bc07..e58c4ab 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -42,28 +42,30 @@ return [...byId.values()].sort((a, b) => Number(a.id) - Number(b.id)); } - // Reads existing config JSON from the edit page's textarea (raw value, - // not affected by the markdown plugin). - async function readExistingConfig(editUrl) { - try { - const doc = await fetchDoc(editUrl); - const ta = doc.querySelector('textarea[name="lf_description"]'); - if (!ta) return null; - const raw = (ta.textContent || '').trim(); - if (!raw) return null; - return JSON.parse(raw); - } catch (e) { - return null; - } - } - - async function getTokenFrom(url) { - const doc = await fetchDoc(url); + // Reads the shared edit page (GET /admin/shaare?post=): + // - Shaarli returns the editlink form pre-filled with the existing + // bookmark (lf_id populated) when the URL already exists, or a blank + // edit form (no lf_id) when creating new. + // Returns { token, lfId, existing } where existing is the parsed JSON + // description (or null). + async function readEditForm(postUrl) { + const basePath = getBasePath(); + const src = basePath + '/admin/shaare?post=' + encodeURIComponent(postUrl); + const doc = await fetchDoc(src); const tokenEl = doc.querySelector('input[name="token"]'); const idEl = doc.querySelector('input[name="lf_id"]'); + const ta = doc.querySelector('textarea[name="lf_description"]'); + let existing = null; + if (ta) { + const raw = (ta.textContent || '').trim(); + if (raw) { + try { existing = JSON.parse(raw); } catch (e) { /* non-JSON */ } + } + } return { token: tokenEl ? tokenEl.value : '', lfId: idEl ? idEl.value : '', + existing, }; } @@ -77,18 +79,27 @@ } // Merge partial updates into the persisted config and save to the single - // bookmark. Deletes any duplicates left over from previous buggy saves. + // bookmark. Creates one when none exists, updates the existing one, and + // deletes any duplicates left over from previous buggy saves. async function save(partial) { // Serialize so rapid toggles do not race and create duplicates. const run = async () => { const basePath = getBasePath(); - const candidates = await findCandidates(); - const primary = candidates[0] || null; - const duplicates = candidates.slice(1); - // Build merged config - let existing = null; - if (primary) existing = await readExistingConfig(primary.editUrl); + // Collect duplicates via tag search (best-effort) + const candidates = await findCandidates(); + + // Always read the edit form for CONFIG_URL: Shaarli returns the + // existing bookmark pre-filled (with lf_id) when the URL exists, + // or a blank edit form (no lf_id) otherwise. This path works for + // both create and update and self-heals if search missed it. + const { token, lfId, existing } = await readEditForm(CONFIG_URL); + if (!token) { + console.warn('[shaarit] theme-config: no CSRF token from edit form'); + return { ok: false, reason: 'no-token' }; + } + + // Merge config (preserve any existing fields not in partial) const base = existing && typeof existing === 'object' ? existing : {}; const config = Object.assign( { version: 2, themes: [], default: 'DEFAULT', mode: 'dark' }, @@ -96,11 +107,6 @@ partial || {} ); - // Get token + lf_id from the primary's edit page (or add page) - const src = primary ? primary.editUrl : basePath + '/admin/add-shaare'; - const { token, lfId } = await getTokenFrom(src); - if (!token) return { ok: false, reason: 'no-token' }; - const fd = new FormData(); fd.append('lf_title', CONFIG_TITLE); fd.append('lf_url', CONFIG_URL); @@ -117,13 +123,15 @@ credentials: 'same-origin', }); - // Clean up any leftover duplicates (reuse the same token, Shaarli - // accepts the session token for both edit and delete). - for (const dup of duplicates) { - await deleteBookmark(dup.id, token); + // Delete any duplicates left over from previous buggy saves, + // keeping the one we just saved (lfId) or the first candidate. + const keepId = lfId || (candidates[0] && candidates[0].id) || null; + for (const c of candidates) { + if (keepId && String(c.id) === String(keepId)) continue; + await deleteBookmark(c.id, token); } - return { ok: true, id: lfId || (primary && primary.id) || null, config }; + return { ok: true, id: keepId, config, created: !lfId }; }; const p = (inflight ? inflight.catch(() => {}) : Promise.resolve()).then(run);