import json import re import secrets import threading from db import get_db, return_db, get_setting, set_setting from templates import wrap_page _request_local = threading.local() def _get_csrf_token(): return getattr(_request_local, 'csrf_token', '') def _csrf_field(): return f'' def _check_csrf(body): token = body.get("_csrf", [""])[0] expected = _get_csrf_token() if not expected or not token: return False return secrets.compare_digest(token, expected) _STOPWORDS = frozenset({ "a", "an", "the", "and", "or", "but", "is", "are", "was", "were", "in", "on", "at", "to", "for", "of", "with", "by", "from", "as", "into", "about", "how", "what", "which", "who", "where", "when", "do", "does", "did", "be", "been", "being", "have", "has", "had", "it", "its", "this", "that", "not", "no", "so", "if", "can", "will", "my", "your", "i", "me", "we", "you", "he", "she", "they", }) def _sanitize_fts_query(query): words = query.split() if not words: return '""' tokens = [] last_idx = len(words) - 1 for i, w in enumerate(words): cleaned = re.sub(r'["\'\(\)\*\+\-\^~:]', '', w).strip() if not cleaned: continue if cleaned.lower() in _STOPWORDS: continue if cleaned.upper() in ("AND", "OR", "NOT", "NEAR"): continue if i == last_idx: tokens.append(f"{cleaned}*") else: tokens.append(f'"{cleaned}"') return " ".join(tokens) if tokens else '""' def _get_bookmark_token(): token = get_setting("bookmark_token") if not token: token = secrets.token_hex(16) set_setting("bookmark_token", token) return token def _respond(body_html, status=200, use_default=False): return { "status": status, "content_type": "text/html; charset=utf-8", "body": wrap_page(body_html, use_default=use_default), "headers": {}, } def _redirect(location): if not location.startswith("/") or location.startswith("//"): location = "/" return { "status": 302, "content_type": "text/html; charset=utf-8", "body": "", "headers": {"Location": location}, } def _json_response(data, status=200, headers=None): return { "status": status, "content_type": "application/json", "body": json.dumps(data, indent=2), "headers": headers or {}, } def _text_response(text, status=200, headers=None): return { "status": status, "content_type": "text/plain", "body": text, "headers": headers or {}, } def _error(status): return _respond(f"
{" | ".join(parts)}
' def _get_page_tags(page_id, db=None): close = False if db is None: db = get_db() close = True rows = db.execute( "SELECT t.name FROM tags t JOIN page_tags pt ON t.id = pt.tag_id " "WHERE pt.page_id = ? ORDER BY t.name", (page_id,) ).fetchall() if close: return_db(db) return [r["name"] for r in rows] def _set_page_tags(page_id, tag_string, db=None): close = False if db is None: db = get_db() close = True db.execute("DELETE FROM page_tags WHERE page_id = ?", (page_id,)) for name in (t.strip().lower() for t in tag_string.split(",") if t.strip()): db.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (name,)) tag_id = db.execute("SELECT id FROM tags WHERE name = ?", (name,)).fetchone()["id"] db.execute("INSERT OR IGNORE INTO page_tags (page_id, tag_id) VALUES (?, ?)", (page_id, tag_id)) if close: db.commit() return_db(db) def _cleanup_orphaned_tags(db): db.execute("DELETE FROM tags WHERE id NOT IN (SELECT DISTINCT tag_id FROM page_tags)")