From c10aa7955c8bd3f98d6f10bdabe36d09f076819d Mon Sep 17 00:00:00 2001 From: Derick Phan Date: Thu, 26 Mar 2026 11:18:47 -0700 Subject: [PATCH] Fix SSRF redirect bypass, identity permissions, error leakage, and DB connection leaks - SSRF: disable automatic redirects, manually follow up to 5 hops with IP re-validation at each step to prevent redirect-to-localhost bypass - Identity file: enforce 0600 permissions on tinyweb_identity at load and creation to prevent other users from reading the private key - Error messages: replace raw exception strings with generic messages to avoid leaking internal paths/hostnames to the UI - DB connections: wrap all get_db() usage in try/finally to guarantee close() even when handlers throw mid-operation Co-Authored-By: Claude Opus 4.6 --- app.py | 5 + db.py | 13 +- handlers.py | 548 ++++++++++++++++++++++++++++------------------------ 3 files changed, 310 insertions(+), 256 deletions(-) diff --git a/app.py b/app.py index 6f520d2..01c4541 100644 --- a/app.py +++ b/app.py @@ -15,9 +15,14 @@ IDENTITY_FILE = "tinyweb_identity" def load_or_create_identity(): if os.path.isfile(IDENTITY_FILE): + # Ensure identity file is only readable by owner + current = os.stat(IDENTITY_FILE).st_mode & 0o777 + if current != 0o600: + os.chmod(IDENTITY_FILE, 0o600) return RNS.Identity.from_file(IDENTITY_FILE) identity = RNS.Identity() identity.to_file(IDENTITY_FILE) + os.chmod(IDENTITY_FILE, 0o600) return identity diff --git a/db.py b/db.py index 945760e..b26a86a 100644 --- a/db.py +++ b/db.py @@ -201,7 +201,18 @@ def get_site_name(): def fetch_page(url): _validate_url_target(url) - resp = requests.get(url, timeout=10, headers={"User-Agent": "TinyWeb/1.0"}) + resp = requests.get(url, timeout=10, headers={"User-Agent": "TinyWeb/1.0"}, allow_redirects=False) + # Follow redirects manually, re-validating each target + max_redirects = 5 + while resp.is_redirect and max_redirects > 0: + redirect_url = resp.headers.get("Location") + if not redirect_url: + break + redirect_url = urljoin(url, redirect_url) + _validate_url_target(redirect_url) + url = redirect_url + resp = requests.get(url, timeout=10, headers={"User-Agent": "TinyWeb/1.0"}, allow_redirects=False) + max_redirects -= 1 resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") diff --git a/handlers.py b/handlers.py index a16dce9..c47f670 100644 --- a/handlers.py +++ b/handlers.py @@ -121,110 +121,111 @@ def _set_page_tags(page_id, tag_string, db=None): def handle_search(query): q = query.get("q", [""])[0].strip() db = get_db() - count = db.execute("SELECT count(*) FROM pages").fetchone()[0] - name = get_site_name() + try: + count = db.execute("SELECT count(*) FROM pages").fetchone()[0] + name = get_site_name() - result_html = "" - trusted_html = "" - if q: - try: - rows = db.execute( - "SELECT p.id, p.url, p.title, p.body, p.note " - "FROM pages_fts f JOIN pages p ON f.rowid = p.id " - "WHERE pages_fts MATCH ? ORDER BY rank LIMIT 50", - (_sanitize_fts_query(q),), + result_html = "" + trusted_html = "" + if q: + try: + rows = db.execute( + "SELECT p.id, p.url, p.title, p.body, p.note " + "FROM pages_fts f JOIN pages p ON f.rowid = p.id " + "WHERE pages_fts MATCH ? ORDER BY rank LIMIT 50", + (_sanitize_fts_query(q),), + ).fetchall() + except Exception: + rows = [] + if rows: + for r in rows: + note_html = "" + if r["note"]: + note_html = f'
{esc(r["note"])}
' + tags = _get_page_tags(r["id"], db) + tags_html = "" + if tags: + tag_links = " ".join(f'[{esc(t)}]' for t in tags) + tags_html = f'
{tag_links}
' + result_html += ( + f'
' + f'{esc(r["title"])}
' + f'{esc(r["url"])}
' + f'{esc(snippet(r["body"], q))}' + f'{note_html}{tags_html}' + f'
' + ) + else: + result_html = "

No results in your index.

" + + # search all linked pages from trusted sites + words = q.lower().split() + all_links = db.execute( + "SELECT l.url, l.label, p.title AS source_title " + "FROM links l JOIN pages p ON l.page_id = p.id", ).fetchall() - except Exception: - rows = [] - if rows: - for r in rows: - note_html = "" - if r["note"]: - note_html = f'
{esc(r["note"])}
' - tags = _get_page_tags(r["id"], db) - tags_html = "" - if tags: - tag_links = " ".join(f'[{esc(t)}]' for t in tags) - tags_html = f'
{tag_links}
' - result_html += ( - f'
' - f'{esc(r["title"])}
' - f'{esc(r["url"])}
' - f'{esc(snippet(r["body"], q))}' - f'{note_html}{tags_html}' - f'
' + indexed_urls = set(r["url"] for r in rows) if rows else set() + seen = set() + trusted = [] + for l in all_links: + if l["url"] in indexed_urls or l["url"] in seen: + continue + if any(w in l["label"].lower() for w in words): + seen.add(l["url"]) + trusted.append(l) + if len(trusted) >= 20: + break + + if trusted: + items = "" + for l in trusted: + items += ( + f'
  • {esc(l["label"])} ' + f'— from {esc(l["source_title"])}
  • ' + ) + trusted_html = ( + f'
    ' + f'from your trusted sites ({len(trusted)})' + f'
      {items}
    ' + f'
    ' ) - else: - result_html = "

    No results in your index.

    " - # search all linked pages from trusted sites - words = q.lower().split() - all_links = db.execute( - "SELECT l.url, l.label, p.title AS source_title " - "FROM links l JOIN pages p ON l.page_id = p.id", - ).fetchall() - indexed_urls = set(r["url"] for r in rows) if rows else set() - seen = set() - trusted = [] - for l in all_links: - if l["url"] in indexed_urls or l["url"] in seen: - continue - if any(w in l["label"].lower() for w in words): - seen.add(l["url"]) - trusted.append(l) - if len(trusted) >= 20: - break + # search synced pages from subscriptions + try: + remote_rows = db.execute( + "SELECT rp.url, rp.title, rp.note, s.name AS source_name " + "FROM remote_pages_fts rpf " + "JOIN remote_pages rp ON rpf.rowid = rp.id " + "JOIN subscriptions s ON rp.subscription_id = s.id " + "WHERE remote_pages_fts MATCH ? ORDER BY rank LIMIT 50", + (_sanitize_fts_query(q),), + ).fetchall() + except Exception: + remote_rows = [] - if trusted: - items = "" - for l in trusted: - items += ( - f'
  • {esc(l["label"])} ' - f'— from {esc(l["source_title"])}
  • ' + remote_html = "" + if q and remote_rows: + # group by source + by_source = {} + for r in remote_rows: + source = r["source_name"] or "unknown" + by_source.setdefault(source, []).append(r) + for source, items in by_source.items(): + source_items = "" + for r in items: + note_html = f' — {esc(r["note"])}' if r["note"] else "" + source_items += ( + f'
  • {esc(r["title"])}' + f'{note_html} ({esc(r["url"])})
  • ' + ) + remote_html += ( + f'
    ' + f'from {esc(source)} ({len(items)})' + f'
      {source_items}
    ' + f'
    ' ) - trusted_html = ( - f'
    ' - f'from your trusted sites ({len(trusted)})' - f'
      {items}
    ' - f'
    ' - ) - - # search synced pages from subscriptions - try: - remote_rows = db.execute( - "SELECT rp.url, rp.title, rp.note, s.name AS source_name " - "FROM remote_pages_fts rpf " - "JOIN remote_pages rp ON rpf.rowid = rp.id " - "JOIN subscriptions s ON rp.subscription_id = s.id " - "WHERE remote_pages_fts MATCH ? ORDER BY rank LIMIT 50", - (_sanitize_fts_query(q),), - ).fetchall() - except Exception: - remote_rows = [] - - remote_html = "" - if q and remote_rows: - # group by source - by_source = {} - for r in remote_rows: - source = r["source_name"] or "unknown" - by_source.setdefault(source, []).append(r) - for source, items in by_source.items(): - source_items = "" - for r in items: - note_html = f' — {esc(r["note"])}' if r["note"] else "" - source_items += ( - f'
  • {esc(r["title"])}' - f'{note_html} ({esc(r["url"])})
  • ' - ) - remote_html += ( - f'
    ' - f'from {esc(source)} ({len(items)})' - f'
      {source_items}
    ' - f'
    ' - ) - - db.close() + finally: + db.close() sub_count = "" if q and remote_rows: sub_count = f" + {len(remote_rows)} from subscriptions" @@ -266,34 +267,40 @@ def handle_add_submit(body): title = index_url(url, note) if tags: db = get_db() - row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone() - if row: - _set_page_tags(row["id"], tags, db) - db.commit() - db.close() + try: + row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone() + if row: + _set_page_tags(row["id"], tags, db) + db.commit() + finally: + db.close() return handle_add_form(f'Indexed: {esc(title)}') - except Exception as e: + except ValueError as e: return handle_add_form(f"Error: {esc(str(e))}") + except Exception: + return handle_add_form("Error: could not fetch or index that URL.") def handle_pages(): db = get_db() - rows = db.execute("SELECT id, url, title, note FROM pages ORDER BY id DESC").fetchall() - items = "" - for r in rows: - note_html = f' — {esc(r["note"])}' if r["note"] else "" - tags = _get_page_tags(r["id"], db) - tags_html = "" - if tags: - tag_links = " ".join(f'[{esc(t)}]' for t in tags) - tags_html = f' {tag_links}' - items += ( - f'
  • {esc(r["title"])}{note_html}{tags_html} ' - f'({esc(r["url"])}) ' - f'edit ' - f'remove
  • ' - ) - db.close() + try: + rows = db.execute("SELECT id, url, title, note FROM pages ORDER BY id DESC").fetchall() + items = "" + for r in rows: + note_html = f' — {esc(r["note"])}' if r["note"] else "" + tags = _get_page_tags(r["id"], db) + tags_html = "" + if tags: + tag_links = " ".join(f'[{esc(t)}]' for t in tags) + tags_html = f' {tag_links}' + items += ( + f'
  • {esc(r["title"])}{note_html}{tags_html} ' + f'({esc(r["url"])}) ' + f'edit ' + f'remove
  • ' + ) + finally: + db.close() return _respond( f"

    indexed pages ({len(rows)})

    " f"" @@ -304,12 +311,13 @@ def handle_pages(): def handle_edit_form(page_id, msg=""): db = get_db() - row = db.execute("SELECT id, url, title, note FROM pages WHERE id = ?", (page_id,)).fetchone() - if not row: + try: + row = db.execute("SELECT id, url, title, note FROM pages WHERE id = ?", (page_id,)).fetchone() + if not row: + return _error(404) + tags = ", ".join(_get_page_tags(page_id, db)) + finally: db.close() - return _error(404) - tags = ", ".join(_get_page_tags(page_id, db)) - db.close() return _respond( f"

    edit page

    " f"

    {esc(row['title'])}
    " @@ -329,17 +337,21 @@ def handle_edit_submit(page_id, body): note = body.get("note", [""])[0].strip() tags = body.get("tags", [""])[0].strip() db = get_db() - db.execute("UPDATE pages SET note = ? WHERE id = ?", (note, page_id)) - _set_page_tags(page_id, tags, db) - db.commit() - db.close() + try: + db.execute("UPDATE pages SET note = ? WHERE id = ?", (note, page_id)) + _set_page_tags(page_id, tags, db) + db.commit() + finally: + db.close() return _redirect("/pages") def handle_delete_confirm(page_id): db = get_db() - row = db.execute("SELECT id, url, title FROM pages WHERE id = ?", (page_id,)).fetchone() - db.close() + try: + row = db.execute("SELECT id, url, title FROM pages WHERE id = ?", (page_id,)).fetchone() + finally: + db.close() if not row: return _error(404) return _respond( @@ -356,11 +368,13 @@ def handle_delete_confirm(page_id): def handle_delete(page_id): db = get_db() - db.execute("DELETE FROM page_tags WHERE page_id = ?", (page_id,)) - db.execute("DELETE FROM links WHERE page_id = ?", (page_id,)) - db.execute("DELETE FROM pages WHERE id = ?", (page_id,)) - db.commit() - db.close() + try: + db.execute("DELETE FROM page_tags WHERE page_id = ?", (page_id,)) + db.execute("DELETE FROM links WHERE page_id = ?", (page_id,)) + db.execute("DELETE FROM pages WHERE id = ?", (page_id,)) + db.commit() + finally: + db.close() return _redirect("/pages") @@ -382,8 +396,10 @@ def handle_bookmark(query): def handle_export(): db = get_db() - rows = db.execute("SELECT url, title, note FROM pages ORDER BY id").fetchall() - db.close() + try: + rows = db.execute("SELECT url, title, note FROM pages ORDER BY id").fetchall() + finally: + db.close() data = [{"url": r["url"], "title": r["title"], "note": r["note"]} for r in rows] return _json_response(data, headers={"Content-Disposition": "attachment; filename=tinyweb-export.json"}) @@ -482,10 +498,12 @@ def handle_about(): dest_hash = get_setting("dest_hash") sharing = get_setting("sharing_enabled", "0") == "1" db = get_db() - page_count = db.execute("SELECT count(*) FROM pages").fetchone()[0] - tag_count = db.execute("SELECT count(DISTINCT tag_id) FROM page_tags").fetchone()[0] - sub_count = db.execute("SELECT count(*) FROM subscriptions").fetchone()[0] - db.close() + try: + page_count = db.execute("SELECT count(*) FROM pages").fetchone()[0] + tag_count = db.execute("SELECT count(DISTINCT tag_id) FROM page_tags").fetchone()[0] + sub_count = db.execute("SELECT count(*) FROM subscriptions").fetchone()[0] + finally: + db.close() sharing_html = ( '

    This instance shares its index publicly. Subscribe to join the network.

    ' @@ -531,12 +549,14 @@ def handle_about(): def handle_tags(): db = get_db() - rows = db.execute( - "SELECT t.name, COUNT(pt.page_id) AS cnt FROM tags t " - "JOIN page_tags pt ON t.id = pt.tag_id " - "GROUP BY t.id ORDER BY t.name" - ).fetchall() - db.close() + try: + rows = db.execute( + "SELECT t.name, COUNT(pt.page_id) AS cnt FROM tags t " + "JOIN page_tags pt ON t.id = pt.tag_id " + "GROUP BY t.id ORDER BY t.name" + ).fetchall() + finally: + db.close() items = "" for r in rows: items += f'
  • {esc(r["name"])} ({r["cnt"]})
  • ' @@ -549,23 +569,25 @@ def handle_tags(): def handle_tag_browse(tag_name): db = get_db() - rows = db.execute( - "SELECT p.id, p.url, p.title, p.note FROM pages p " - "JOIN page_tags pt ON p.id = pt.page_id " - "JOIN tags t ON t.id = pt.tag_id " - "WHERE t.name = ? ORDER BY p.id DESC", - (tag_name,), - ).fetchall() - items = "" - for r in rows: - note_html = f' — {esc(r["note"])}' if r["note"] else "" - tags = _get_page_tags(r["id"], db) - tag_links = " ".join(f'[{esc(t)}]' for t in tags) - items += ( - f'
  • {esc(r["title"])}{note_html} {tag_links} ' - f'({esc(r["url"])})
  • ' - ) - db.close() + try: + rows = db.execute( + "SELECT p.id, p.url, p.title, p.note FROM pages p " + "JOIN page_tags pt ON p.id = pt.page_id " + "JOIN tags t ON t.id = pt.tag_id " + "WHERE t.name = ? ORDER BY p.id DESC", + (tag_name,), + ).fetchall() + items = "" + for r in rows: + note_html = f' — {esc(r["note"])}' if r["note"] else "" + tags = _get_page_tags(r["id"], db) + tag_links = " ".join(f'[{esc(t)}]' for t in tags) + items += ( + f'
  • {esc(r["title"])}{note_html} {tag_links} ' + f'({esc(r["url"])})
  • ' + ) + finally: + db.close() return _respond( f'

    tag: {esc(tag_name)}

    ' f'

    {len(rows)} page(s)

    ' @@ -582,20 +604,24 @@ def handle_api_sites(): headers={"Access-Control-Allow-Origin": "*"}, ) db = get_db() - rows = db.execute("SELECT id, url, title, note FROM pages ORDER BY id DESC").fetchall() - sites = [] - for r in rows: - tags = _get_page_tags(r["id"], db) - sites.append({"url": r["url"], "title": r["title"], "note": r["note"], "tags": tags}) - db.close() + try: + rows = db.execute("SELECT id, url, title, note FROM pages ORDER BY id DESC").fetchall() + sites = [] + for r in rows: + tags = _get_page_tags(r["id"], db) + sites.append({"url": r["url"], "title": r["title"], "note": r["note"], "tags": tags}) + finally: + db.close() data = {"name": get_site_name(), "sites": sites} return _json_response(data, headers={"Access-Control-Allow-Origin": "*"}) def handle_subscriptions(msg=""): db = get_db() - subs = db.execute("SELECT * FROM subscriptions ORDER BY id DESC").fetchall() - db.close() + try: + subs = db.execute("SELECT * FROM subscriptions ORDER BY id DESC").fetchall() + finally: + db.close() items = "" for s in subs: auto_label = "on" if s["auto_sync"] else "off" @@ -651,8 +677,8 @@ def handle_subscription_add(body): name = data.get("name", "") except PermissionError: return handle_subscriptions("That instance has sharing disabled.") - except Exception as e: - return handle_subscriptions(f"Could not reach that instance: {esc(str(e))}") + except Exception: + return handle_subscriptions("Could not reach that instance.") db = get_db() try: db.execute( @@ -668,18 +694,19 @@ def handle_subscription_add(body): def handle_subscription_browse(sub_id): db = get_db() - sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone() - if not sub: - db.close() - return _error(404) - local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages").fetchall()) + try: + sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone() + if not sub: + return _error(404) + local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages").fetchall()) - # Use locally synced data if available, otherwise fetch live - remote_rows = db.execute( - "SELECT url, title, note, tags FROM remote_pages WHERE subscription_id = ?", - (sub_id,), - ).fetchall() - db.close() + # Use locally synced data if available, otherwise fetch live + remote_rows = db.execute( + "SELECT url, title, note, tags FROM remote_pages WHERE subscription_id = ?", + (sub_id,), + ).fetchall() + finally: + db.close() if remote_rows: sites = [] @@ -692,8 +719,8 @@ def handle_subscription_browse(sub_id): sites = data.get("sites", []) except PermissionError: return handle_subscriptions("That instance has sharing disabled.") - except Exception as e: - return handle_subscriptions(f"Could not fetch sites: {esc(str(e))}") + except Exception: + return handle_subscriptions("Could not fetch sites from that instance.") new_items = "" existing_items = "" @@ -739,17 +766,19 @@ def handle_subscription_pick(body): # Build a url->tags map from remote_pages for this subscription db = get_db() - remote_rows = db.execute( - "SELECT url, tags FROM remote_pages WHERE subscription_id = ?", (sub_id,) - ).fetchall() - remote_tags = {r["url"]: r["tags"] for r in remote_rows} + try: + remote_rows = db.execute( + "SELECT url, tags FROM remote_pages WHERE subscription_id = ?", (sub_id,) + ).fetchall() + remote_tags = {r["url"]: r["tags"] for r in remote_rows} - if import_all: - local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages").fetchall()) - urls = [r["url"] for r in remote_rows if r["url"] not in local_urls] - else: - urls = body.get("urls", []) - db.close() + if import_all: + local_urls = set(r["url"] for r in db.execute("SELECT url FROM pages").fetchall()) + urls = [r["url"] for r in remote_rows if r["url"] not in local_urls] + else: + urls = body.get("urls", []) + finally: + db.close() if not urls: return handle_subscriptions("No sites selected.") @@ -763,11 +792,13 @@ def handle_subscription_pick(body): tags_str = remote_tags.get(url, "") if tags_str: db = get_db() - row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone() - if row: - _set_page_tags(row["id"], tags_str, db) - db.commit() - db.close() + try: + row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone() + if row: + _set_page_tags(row["id"], tags_str, db) + db.commit() + finally: + db.close() imported += 1 except Exception: errors += 1 @@ -776,62 +807,67 @@ def handle_subscription_pick(body): def handle_subscription_sync(sub_id): db = get_db() - sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone() - if not sub: - db.close() - return handle_subscriptions("Subscription not found.") try: - data = fetch_remote_sites(sub["dest_hash"]) - sites = data.get("sites", []) - remote_name = data.get("name", sub["name"]) - except PermissionError: - db.close() - return handle_subscriptions("That instance has sharing disabled.") - except Exception as e: - db.close() - return handle_subscriptions(f"Could not sync: {esc(str(e))}") - - # Clear old remote pages for this subscription and re-insert - db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,)) - synced = 0 - for s in sites: + sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone() + if not sub: + return handle_subscriptions("Subscription not found.") try: - tags_str = ",".join(s.get("tags", [])) - db.execute( - "INSERT INTO remote_pages (subscription_id, url, title, note, tags) VALUES (?, ?, ?, ?, ?)", - (sub_id, s["url"], s["title"], s.get("note", ""), tags_str), - ) - synced += 1 + data = fetch_remote_sites(sub["dest_hash"]) + sites = data.get("sites", []) + remote_name = data.get("name", sub["name"]) + except PermissionError: + return handle_subscriptions("That instance has sharing disabled.") except Exception: - pass - now = datetime.now().strftime("%Y-%m-%d %H:%M") - db.execute("UPDATE subscriptions SET last_sync = ?, name = ? WHERE id = ?", (now, remote_name, sub_id)) - db.commit() - db.close() + return handle_subscriptions("Could not sync with that instance.") + + # Clear old remote pages for this subscription and re-insert + db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,)) + synced = 0 + for s in sites: + try: + tags_str = ",".join(s.get("tags", [])) + db.execute( + "INSERT INTO remote_pages (subscription_id, url, title, note, tags) VALUES (?, ?, ?, ?, ?)", + (sub_id, s["url"], s["title"], s.get("note", ""), tags_str), + ) + synced += 1 + except Exception: + pass + now = datetime.now().strftime("%Y-%m-%d %H:%M") + db.execute("UPDATE subscriptions SET last_sync = ?, name = ? WHERE id = ?", (now, remote_name, sub_id)) + db.commit() + finally: + db.close() return handle_subscriptions(f"Synced {synced} site(s) from {esc(remote_name)}.") def handle_subscription_autosync(sub_id): db = get_db() - db.execute("UPDATE subscriptions SET auto_sync = 1 - auto_sync WHERE id = ?", (sub_id,)) - db.commit() - db.close() + try: + db.execute("UPDATE subscriptions SET auto_sync = 1 - auto_sync WHERE id = ?", (sub_id,)) + db.commit() + finally: + db.close() return _redirect("/subscriptions") def handle_subscription_delete(sub_id): db = get_db() - db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,)) - db.execute("DELETE FROM subscriptions WHERE id = ?", (sub_id,)) - db.commit() - db.close() + try: + db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,)) + db.execute("DELETE FROM subscriptions WHERE id = ?", (sub_id,)) + db.commit() + finally: + db.close() return _redirect("/subscriptions") def handle_subscription_syncall(): db = get_db() - subs = db.execute("SELECT * FROM subscriptions WHERE auto_sync = 1").fetchall() - db.close() + try: + subs = db.execute("SELECT * FROM subscriptions WHERE auto_sync = 1").fetchall() + finally: + db.close() if not subs: return handle_subscriptions("No subscriptions have auto-sync enabled.") total = 0 @@ -841,20 +877,22 @@ def handle_subscription_syncall(): sites = data.get("sites", []) remote_name = data.get("name", sub["name"]) db = get_db() - db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub["id"],)) - for s in sites: - try: - tags_str = ",".join(s.get("tags", [])) - db.execute( - "INSERT INTO remote_pages (subscription_id, url, title, note, tags) VALUES (?, ?, ?, ?, ?)", - (sub["id"], s["url"], s["title"], s.get("note", ""), tags_str), - ) - except Exception: - pass - now = datetime.now().strftime("%Y-%m-%d %H:%M") - db.execute("UPDATE subscriptions SET last_sync = ?, name = ? WHERE id = ?", (now, remote_name, sub["id"])) - db.commit() - db.close() + try: + db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub["id"],)) + for s in sites: + try: + tags_str = ",".join(s.get("tags", [])) + db.execute( + "INSERT INTO remote_pages (subscription_id, url, title, note, tags) VALUES (?, ?, ?, ?, ?)", + (sub["id"], s["url"], s["title"], s.get("note", ""), tags_str), + ) + except Exception: + pass + now = datetime.now().strftime("%Y-%m-%d %H:%M") + db.execute("UPDATE subscriptions SET last_sync = ?, name = ? WHERE id = ?", (now, remote_name, sub["id"])) + db.commit() + finally: + db.close() total += 1 except Exception: pass