"""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'(]*\bmethod=["\']post["\'][^>]*>)', lambda match: match[1] + field, markup, flags=re.I) def valid_csrf(request, fields): expected = csrf_token(request, create=False) supplied = str(fields.get("csrf") or "") return bool(expected and re.fullmatch(r"[a-f0-9]{64}", supplied) and secrets.compare_digest(expected, supplied))