167 lines
4.6 KiB
Python
167 lines
4.6 KiB
Python
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'<input type="hidden" name="_csrf" value="{_get_csrf_token()}">'
|
|
|
|
|
|
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, head_html=""):
|
|
return {
|
|
"status": status,
|
|
"content_type": "text/html; charset=utf-8",
|
|
"body": wrap_page(body_html, use_default=use_default, head_html=head_html),
|
|
"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"<h1>{status}</h1>", status)
|
|
|
|
|
|
PER_PAGE = 10
|
|
BROWSE_PER_PAGE = 50
|
|
|
|
|
|
def _paginate(query, key="p"):
|
|
try:
|
|
page = int(query.get(key, ["1"])[0])
|
|
except (ValueError, IndexError):
|
|
page = 1
|
|
return max(1, page)
|
|
|
|
|
|
def _page_nav(page, total, base_url, per_page=None):
|
|
per_page = per_page or PER_PAGE
|
|
if total <= per_page:
|
|
return ""
|
|
total_pages = (total + per_page - 1) // per_page
|
|
sep = "&" if "?" in base_url else "?"
|
|
parts = []
|
|
if page > 1:
|
|
parts.append(f'<a href="{base_url}{sep}p={page - 1}">« prev</a>')
|
|
parts.append(f"page {page} of {total_pages}")
|
|
if page < total_pages:
|
|
parts.append(f'<a href="{base_url}{sep}p={page + 1}">next »</a>')
|
|
return f'<p class="pagination">{" | ".join(parts)}</p>'
|
|
|
|
|
|
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)")
|