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_style_template_submit, handle_field_save, handle_about, _set_flash 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", "") scheme = data.get("scheme", "http") 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": prefill_url = query.get("url", [""])[0].strip() return handle_add_form(prefill_url=prefill_url) elif path == "/pages": return handle_pages(query) elif path.startswith("/edit/"): pid = extract_id("/edit/") return handle_edit_form(pid, page=query.get("p", [""])[0]) 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(gateway_host=gateway_host, scheme=scheme) 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 == "/subscriptions/add": return handle_add_form(action_type="subscribe") 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("

403 Forbidden

Invalid or missing CSRF token.

", 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, gateway_host=gateway_host, scheme=scheme) elif path == "/style/template": return handle_style_template_submit(body, gateway_host=gateway_host, scheme=scheme) elif path == "/style/field": return handle_field_save(body) elif path == "/style/reset": set_setting("custom_template", "") _set_flash("Template reset to default.") return _redirect("/style") elif path == "/style/vacuum": from db import vacuum_db vacuum_db() _set_flash("Database vacuumed.") return _redirect("/style") 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