Upload an OPML file to import; tests for every way in and out
- The import screen has a file picker beside the paste box. The page reads the file, checks it looks like OPML before sending, and clears the picker when it is refused and after it is imported. The file is sent as text and never written to disk on the server. - The server parses the OPML before touching anything and answers 400 "that is not an OPML file" (was a 500). subscribe_opml takes a parsed document, so ipx import also refuses a non-OPML file by name. - Tests: Settings' Export OPML download and paste import; uploading an RSS file (refused) and a real OPML; the server's 400; the admin's export round-tripped into a second account; ipx import/export in a scratch config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
This commit is contained in:
@@ -1390,15 +1390,25 @@ function opmlModal(){
|
||||
<div class="cardacts" style="justify-content:flex-start">
|
||||
<a class="btn" href="/api/opml" download="ipx-subscriptions.opml">Export</a>
|
||||
</div>
|
||||
<div class="field" style="margin-top:16px"><label>Import: paste OPML</label>
|
||||
<div class="field" style="margin-top:16px"><label>Import: choose a file, or paste OPML</label>
|
||||
<input type="file" id="opmlFile" accept=".opml,.xml,text/x-opml,text/xml,application/xml" style="margin-bottom:8px">
|
||||
<textarea id="opmlText" rows="6" style="width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:8px;font:12px monospace"></textarea></div>
|
||||
<div class="cardacts"><button class="btn" onclick="closeModal()">Close</button>
|
||||
<button class="btn primary" id="oimp">Import</button></div>`);
|
||||
$('#oimp').onclick=async()=>{
|
||||
// A chosen file is read here and sent as text, so the server never stores it. Clearing
|
||||
// the picker lets go of it on this side too, whether it was refused or imported.
|
||||
const pick=$('#opmlFile'), file=pick.files[0];
|
||||
const xml=file ? await file.text() : $('#opmlText').value;
|
||||
const letGo=()=>{ pick.value=''; };
|
||||
// A quick look before sending anything. The server parses it properly and has the last word.
|
||||
if(!/<opml[\s>]/i.test(xml)){
|
||||
letGo(); toast(`${file?file.name:'That'} is not an OPML file`,true); return;
|
||||
}
|
||||
try{
|
||||
const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml:$('#opmlText').value})});
|
||||
closeModal(); toast(`Subscribed to ${r.added} feed(s)`+(r.already?`, ${r.already} you already had`:'')); loadFeeds(true);
|
||||
}catch(e){ toast(e.message,true); }
|
||||
const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml})});
|
||||
letGo(); closeModal(); toast(`Subscribed to ${r.added} feed(s)`+(r.already?`, ${r.already} you already had`:'')); loadFeeds(true);
|
||||
}catch(e){ letGo(); toast(e.message,true); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user