From 5ded340997eb1e7bafa00f90e4874e29b744cf0e Mon Sep 17 00:00:00 2001 From: rekushi <1+rekushi@noreply.flipnote.world> Date: Thu, 29 Oct 2026 04:41:32 +0000 Subject: [PATCH] feat(web): add shared pagination and CSRF helpers Generate filter-preserving page links and validate tokens on web forms. --- web_ui.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 web_ui.py diff --git a/web_ui.py b/web_ui.py new file mode 100644 index 0000000..eadbfb5 --- /dev/null +++ b/web_ui.py @@ -0,0 +1,67 @@ +"""Small web controls adapted from RexiMemo's paged settings and lists.""" +from __future__ import annotations + +from html import escape +from http.cookies import SimpleCookie, CookieError +from urllib.parse import urlencode +import math +import re +import secrets + + +def page_number(value, total, per_page): + try: + page = int(value) + except (ValueError, TypeError): + page = 1 + return max(1, min(page, max(1, math.ceil(total / per_page)))) + + +def pagination(page, total, per_page, path, **params): + pages = max(1, math.ceil(total / per_page)) + if pages <= 1: + return "" + def link(number, label, title): + target = path + "?" + urlencode({**params, "page": number}) + return f'{label}' + first = '' + last = '' + parts = [link(1, first, "First page"), link(page-1, "‹", "Previous page")] if page > 1 else [] + for number in range(max(1, page-2), min(pages, page+2)+1): + parts.append(f'{number}' if number == page else link(number, str(number), f"Page {number}")) + if page < pages: + parts.extend([link(page+1, "›", "Next page"), link(pages, last, "Last page")]) + return '' + + +def csrf_token(request, create=True): + if getattr(request, "_ose_csrf", ""): + return request._ose_csrf + headers = request.getAllHeaders() + cookie = next((value for key, value in headers.items() if str(key.decode() if isinstance(key, bytes) else key).lower() == "cookie"), "") + jar = SimpleCookie() + try: + jar.load(cookie.decode() if isinstance(cookie, bytes) else cookie) + token = jar["ose_csrf"].value if "ose_csrf" in jar else "" + except CookieError: + token = "" + if not re.fullmatch(r"[a-f0-9]{64}", token): + if not create: + return "" + token = secrets.token_hex(32) + secure = "; Secure" if getattr(request, "isSecure", lambda: False)() else "" + request.setHeader(b"Set-Cookie", f"ose_csrf={token}; Path=/; HttpOnly; SameSite=Lax{secure}".encode()) + request._ose_csrf = token + return token + + +def protect_forms(request, markup): + token = csrf_token(request) + field = f'' + return re.sub(r'(