using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; using Inversion; using Astralis; namespace Statum { /** * Outcome of {@link StatumPage.select_variant}: either the index of the * pre-rendered variant to serve, or a guard failure (optional redirect or an * error page at a status). */ public class StatumVariantSelection : Object { public bool guard_failed { get; private set; } public string? redirect { get; private set; } public StatusCode status { get; private set; default = StatusCode.FORBIDDEN; } public int variant_index { get; private set; } /** Selects the variant at `index`. */ public static StatumVariantSelection variant(int index) { var selection = new StatumVariantSelection(); selection.variant_index = index; return selection; } /** Signals a guard failure that should redirect to `redirect` (HTTP 302). */ public static StatumVariantSelection redirect_to(string redirect) { var selection = new StatumVariantSelection(); selection.guard_failed = true; selection.redirect = redirect; return selection; } /** Signals a guard failure that should serve an error page at `status`. */ public static StatumVariantSelection failure(StatusCode status = StatusCode.FORBIDDEN) { var selection = new StatumVariantSelection(); selection.guard_failed = true; selection.status = status; return selection; } } /** * Base class for pre-rendered Statum pages. * * A generated subclass (emitted by `statum-mkpstm`) extends this, providing * the precompressed variant byte arrays and a compiled {@link select_variant} * that evaluates `pstm-guard`s and the `pstm-if` matrix against the request's * held slots. This base supplies header parsing → held slots, encoding * negotiation, ETag handling and guard redirect/error-page serving. */ public abstract class StatumPage : Object, Endpoint { protected HeldSlotResolver resolver = inject(); /** Content type of the served page (normally `text/html`). */ public abstract string content_type { get; } /** * Evaluates the page's guards and `pstm-if` matrix against the held * slots, returning the variant to serve or a guard failure. */ public abstract StatumVariantSelection select_variant(ReadOnlyAssociative held) throws GLib.Error; /** Best supported encoding for the chosen variant. */ public abstract string get_best_encoding(int variant, Set supported); /** The bytes for `variant` under `encoding`. */ public abstract unowned uint8[] get_encoding(int variant, string encoding); /** The ETag for `variant` under `encoding`. */ public abstract string get_etag_for(int variant, string encoding); public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws GLib.Error { var held = resolver.resolve(http_context); StatumVariantSelection selection; try { selection = select_variant(held); } catch (GLib.Error e) { return new HttpStringResult(@"Internal error: $(e.message)", StatusCode.INTERNAL_SERVER_ERROR); } if (selection.guard_failed) { return serve_guard_failure(selection); } var accepts = parse_accept_encoding(http_context); var best = get_best_encoding(selection.variant_index, accepts); var etag = get_etag_for(selection.variant_index, best); if (etag_matches(http_context, etag)) { return new HttpEmptyResult(StatusCode.NOT_MODIFIED); } var bytes = get_encoding(selection.variant_index, best); var result = new HttpDataResult(Wrap.byte_array(bytes)); result.set_header("Content-Type", content_type); result.set_header("Content-Encoding", best); result.set_header("ETag", etag); return result; } private static HttpResult serve_guard_failure(StatumVariantSelection selection) { if (selection.redirect != null) { var result = new HttpEmptyResult((StatusCode) 302); result.set_header("Location", (!)selection.redirect); return result; } return new HttpStringResult("Forbidden", selection.status); } private static Set parse_accept_encoding(HttpContext http_context) { var supported = new HashSet(); foreach (var header in http_context.request.headers.get_or_empty("Accept-Encoding")) { foreach (var token in header.split(",")) { var encoding = token.strip(); if (encoding.length > 0) { supported.add(encoding); } } } return supported; } private static bool etag_matches(HttpContext http_context, string etag) { foreach (var header in http_context.request.headers.get_or_empty("If-None-Match")) { if (header.contains(etag)) { return true; } } return false; } } /** * Registers a {@link PropertiesPropertyAccessor} for any {@link Element} * wrapping a {@link Properties} value, so that `pstm-if`/`pstm-guard` * predicate evaluation (the runtime-evaluator fallback) can read members off * a slot's public/private data — e.g. `auth.role` or `auth.private.score`. * * Registered once by {@link StatumModule}. */ public class StatumPropertiesAccessorFactory : Object, PropertyAccessorFactory { public PropertyAccessor? create(Element element) { Properties properties; if (element.try_get_as(out properties)) { return new PropertiesPropertyAccessor(properties); } return null; } } }