- _helpers.py: CSRF, FTS sanitizer, pagination, response helpers, tag helpers - search.py: BM25 + hybrid search, trusted/remote result rendering - pages.py: add/edit/delete/bulk/bookmark handlers - subscriptions.py: sync, share preview, API sites, subscription CRUD - customize.py: settings form, about page - tags.py: tag list and tag browse handlers - data.py: export, import, semantic reindex handlers - __init__.py: dispatch, re-exports, forum_plugin, _request_local All 58 external symbols re-exported. No changes to app.py, conftest.py, or any test file.
200 lines
8 KiB
Python
200 lines
8 KiB
Python
import json
|
|
import secrets
|
|
import threading
|
|
from urllib.parse import unquote
|
|
|
|
from db import get_db, return_db, set_setting
|
|
import templates as templates_mod
|
|
from templates import esc, wrap_page
|
|
from rns_client import fetch_remote_sites
|
|
|
|
from ._helpers import (
|
|
_request_local, _get_csrf_token, _csrf_field, _check_csrf,
|
|
_sanitize_fts_query, _get_bookmark_token,
|
|
_respond, _redirect, _json_response, _text_response, _error,
|
|
PER_PAGE, BROWSE_PER_PAGE, _paginate, _page_nav,
|
|
_get_page_tags, _set_page_tags, _cleanup_orphaned_tags,
|
|
)
|
|
from .search import handle_search
|
|
from .pages import (
|
|
handle_add_form, handle_add_submit, handle_add_manual_submit,
|
|
handle_pages, _render_bulk_delete_confirm, handle_bulk_action,
|
|
handle_edit_form, handle_edit_submit,
|
|
handle_delete_confirm, handle_delete,
|
|
handle_bookmark,
|
|
)
|
|
from .subscriptions import (
|
|
_page_is_shared, _shared_sites, _shared_all_urls, _count_shared_pages,
|
|
handle_share_preview, handle_api_sites,
|
|
handle_subscriptions, handle_subscription_add, handle_subscription_browse,
|
|
handle_subscription_pick, _sync_subscription,
|
|
handle_subscription_sync, handle_subscription_autosync,
|
|
handle_subscription_delete, handle_subscription_syncall,
|
|
_sync_threads,
|
|
)
|
|
from .customize import handle_style_form, handle_style_submit, handle_about
|
|
from .tags import handle_tags, handle_tag_browse
|
|
from .data import (
|
|
handle_export, handle_import_form, handle_import_submit,
|
|
handle_reindex_form, handle_reindex_submit, _reindex_thread,
|
|
)
|
|
|
|
forum_plugin = None
|
|
|
|
|
|
def _dispatch_inner(data):
|
|
method = data.get("method", "GET")
|
|
path = data.get("path", "/")
|
|
query = data.get("query", {})
|
|
body = data.get("body", {})
|
|
gateway_host = data.get("gateway_host", "")
|
|
|
|
def extract_id(prefix):
|
|
try:
|
|
return int(path[len(prefix):])
|
|
except (ValueError, IndexError):
|
|
return None
|
|
|
|
if method == "GET":
|
|
if path == "/":
|
|
return handle_search(query)
|
|
elif path == "/add":
|
|
action_type = query.get("type", ["index"])[0]
|
|
prefill_url = query.get("url", [""])[0].strip()
|
|
return handle_add_form(
|
|
action_type=action_type if action_type == "subscribe" else "index",
|
|
prefill_url=prefill_url,
|
|
)
|
|
elif path == "/pages":
|
|
return handle_pages(query)
|
|
elif path.startswith("/edit/"):
|
|
pid = extract_id("/edit/")
|
|
return handle_edit_form(pid) if pid is not None else _error(400)
|
|
elif path.startswith("/delete/"):
|
|
pid = extract_id("/delete/")
|
|
return handle_delete_confirm(pid) if pid is not None else _error(400)
|
|
elif path == "/bookmark":
|
|
return handle_bookmark(query)
|
|
elif path == "/style":
|
|
return handle_style_form()
|
|
elif path == "/share/preview":
|
|
return handle_share_preview()
|
|
elif path == "/about":
|
|
return handle_about()
|
|
elif path == "/export":
|
|
return handle_export(query)
|
|
elif path == "/import":
|
|
return handle_import_form()
|
|
elif path == "/tags":
|
|
return handle_tags()
|
|
elif path.startswith("/tags/"):
|
|
tag_name = unquote(path[len("/tags/"):])
|
|
return handle_tag_browse(tag_name, query) if tag_name else _error(400)
|
|
elif path == "/reindex":
|
|
return handle_reindex_form()
|
|
elif path == "/api/sites":
|
|
return handle_api_sites(query)
|
|
elif path == "/subscriptions":
|
|
return handle_subscriptions()
|
|
elif path.startswith("/subscriptions/browse/"):
|
|
sid = extract_id("/subscriptions/browse/")
|
|
return handle_subscription_browse(sid) if sid is not None else _error(400)
|
|
elif path.startswith("/forum"):
|
|
if forum_plugin and forum_plugin.is_enabled():
|
|
return forum_plugin.handle(method, path, query, {}, data.get("cookies", {}))
|
|
return _error(404)
|
|
elif method == "POST":
|
|
if path.startswith("/forum"):
|
|
if forum_plugin and forum_plugin.is_enabled():
|
|
return forum_plugin.handle(method, path, query, body, data.get("cookies", {}))
|
|
return _error(404)
|
|
if not _check_csrf(body):
|
|
return _respond("<h1>403 Forbidden</h1><p>Invalid or missing CSRF token.</p>", status=403)
|
|
if path == "/add":
|
|
return handle_add_submit(body)
|
|
elif path == "/pages/bulk":
|
|
return handle_bulk_action(body)
|
|
elif path == "/add/manual":
|
|
return handle_add_manual_submit(body)
|
|
elif path.startswith("/edit/"):
|
|
pid = extract_id("/edit/")
|
|
return handle_edit_submit(pid, body) if pid is not None else _error(400)
|
|
elif path.startswith("/delete/"):
|
|
pid = extract_id("/delete/")
|
|
return handle_delete(pid) if pid is not None else _error(400)
|
|
elif path == "/style":
|
|
return handle_style_submit(body)
|
|
elif path == "/style/reset":
|
|
set_setting("custom_template", "")
|
|
return handle_style_form("Template reset to default.")
|
|
elif path == "/style/vacuum":
|
|
from db import vacuum_db
|
|
vacuum_db()
|
|
return handle_style_form("Database vacuumed.")
|
|
elif path == "/import":
|
|
return handle_import_submit(body)
|
|
elif path == "/reindex":
|
|
return handle_reindex_submit(body)
|
|
elif path == "/subscriptions/add":
|
|
return handle_subscription_add(body)
|
|
elif path == "/subscriptions/pick":
|
|
return handle_subscription_pick(body)
|
|
elif path.startswith("/subscriptions/sync/"):
|
|
sid = extract_id("/subscriptions/sync/")
|
|
return handle_subscription_sync(sid) if sid is not None else _error(400)
|
|
elif path.startswith("/subscriptions/autosync/"):
|
|
sid = extract_id("/subscriptions/autosync/")
|
|
return handle_subscription_autosync(sid) if sid is not None else _error(400)
|
|
elif path.startswith("/subscriptions/delete/"):
|
|
sid = extract_id("/subscriptions/delete/")
|
|
return handle_subscription_delete(sid) if sid is not None else _error(400)
|
|
elif path == "/subscriptions/syncall":
|
|
return handle_subscription_syncall()
|
|
|
|
return _error(404)
|
|
|
|
|
|
def dispatch_request(data):
|
|
path = data.get("path", "/")
|
|
cookies = data.get("cookies", {})
|
|
|
|
if path.startswith("/forum") and forum_plugin and forum_plugin.is_enabled():
|
|
resp = _dispatch_inner(data)
|
|
resp.setdefault("headers", {})
|
|
resp["headers"]["X-Frame-Options"] = "DENY"
|
|
resp["headers"]["X-Content-Type-Options"] = "nosniff"
|
|
if resp.get("content_type", "").startswith("text/html"):
|
|
resp["body"] = wrap_page(resp.get("body", ""))
|
|
resp["headers"]["Content-Security-Policy"] = (
|
|
"default-src 'self'; "
|
|
"script-src 'self' 'unsafe-inline'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src * data:; "
|
|
"frame-ancestors 'none'; "
|
|
"form-action 'self'; "
|
|
"base-uri 'self'"
|
|
)
|
|
return resp
|
|
|
|
csrf_token = cookies.get("_csrf", "")
|
|
if not csrf_token:
|
|
csrf_token = secrets.token_hex(32)
|
|
_request_local.csrf_token = csrf_token
|
|
|
|
resp = _dispatch_inner(data)
|
|
|
|
resp.setdefault("headers", {})
|
|
resp["headers"]["Set-Cookie"] = f"_csrf={csrf_token}; SameSite=Strict; HttpOnly; Path=/"
|
|
resp["headers"]["X-Frame-Options"] = "DENY"
|
|
resp["headers"]["X-Content-Type-Options"] = "nosniff"
|
|
if resp.get("content_type", "").startswith("text/html"):
|
|
resp["headers"]["Content-Security-Policy"] = (
|
|
"default-src 'self'; "
|
|
"script-src 'self' 'unsafe-inline'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src * data:; "
|
|
"frame-ancestors 'none'; "
|
|
"form-action 'self'; "
|
|
"base-uri 'self'"
|
|
)
|
|
return resp
|