Help for web.cookies

Sample CLI

gway web.cookies append

Full Code

def append(name: str, label: str, value: str, sep: str = "|") -> list:
    """
    Append a (label=value) entry to the specified cookie, ensuring no duplicates (label-based).
    Useful for visited history, shopping cart items, etc.
    """
    if not check_consent():
        return []
    raw = get(name, "")
    items = raw.split(sep) if raw else []
    label_norm = label.lower()
    # Remove existing with same label
    items = [v for v in items if not (v.split("=", 1)[0].lower() == label_norm)]
    items.append(f"{label}={value}")
    cookie_value = sep.join(items)
    set(name, cookie_value)
    return items

Sample CLI

gway web.cookies check-consent

Full Code

def check_consent() -> bool:
    """
    Returns True if the user has accepted cookies (not blank, not None).
    """
    cookie_value = get("cookies_accepted")
    return cookie_value == "yes"

Sample CLI

gway web.cookies clear-all

Full Code

def clear_all(path="/"):
    """
    Remove all cookies in the request, blanking and expiring each.
    """
    if not check_consent():
        return
    for cookie in list(request.cookies):
        remove(cookie, path=path)

Sample CLI

gway web.cookies get

Full Code

def get(name: str, default=None):
    """Get a cookie value from the request. Returns None if blank or unset."""
    val = request.get_cookie(name, default)
    return None if (val is None or val == "") else val

Sample CLI

gway web.cookies list-all

Full Code

def list_all() -> dict:
    """
    Returns a dict of all cookies from the request, omitting blanked cookies.
    """
    if not check_consent():
        return {}
    return {k: v for k, v in request.cookies.items() if v not in (None, "")}

Sample CLI

gway web.cookies remove

Full Code

def remove(name, *args, **kwargs):
    _orig_remove(name, *args, **kwargs)
    update_mask_on_cookie_change()

Sample CLI

gway web.cookies set

Full Code

def set(name, value, *args, **kwargs):
    _orig_set(name, value, *args, **kwargs)
    update_mask_on_cookie_change()

Sample CLI

gway web.cookies update-mask-on-cookie-change

Full Code

def update_mask_on_cookie_change():
    """
    Called when any cookie is set or removed, to update the mask record (if any) in masks.cdv.
    """
    mask = get("mask")
    if mask:
        norm = _normalize_mask(mask)
        if not norm:
            return
        mask_map = _read_masks()
        current = {k: v for k, v in _get_current_cookies().items() if k not in ("mask", "cookies_accepted")}
        mask_map[norm] = current
        _write_masks(mask_map)

Sample CLI

gway web.cookies view-accept

Full Code

def view_accept(*, next="/cookies/cookie-jar"):
    set("cookies_accepted", "yes")
    response.status = 303
    response.set_header("Location", next)
    return ""

Sample CLI

gway web.cookies view-cookie-jar

Full Code

def view_cookie_jar(*, eat=None):
    cookies_ok = check_consent()
    # Handle eating a cookie (removal via ?eat=)
    if cookies_ok and eat:
        eat_key = str(eat)
        eat_key_norm = eat_key.strip().lower()
        if eat_key_norm not in ("cookies_accepted", "cookies_eaten") and eat_key in request.cookies:
            remove(eat_key)
            try:
                eaten_count = int(get("cookies_eaten") or "0")
            except Exception:
                eaten_count = 0
            set("cookies_eaten", str(eaten_count + 1))
            response.status = 303
            response.set_header("Location", "/cookies/cookie-jar")
            return ""

    def describe_cookie(key, value):
        key = html.escape(key or "")
        value = html.escape(value or "")
        protected = key in ("cookies_accepted", "cookies_eaten")
        x_link = ""
        if not protected:
            x_link = (
                f" <a href='/cookies/cookie-jar?eat={key}' "
                "style='color:#a00;text-decoration:none;font-weight:bold;font-size:1.1em;margin-left:0.5em;' "
                "title='Remove this cookie' onclick=\"return confirm('Remove cookie: {0}?');\">[X]</a>".format(key)
            )
        if not value:
            return f"<li><b>{key}</b>: (empty)</li>"
        if key == "visited":
            items = value.split("|")
            links = "".join(
                f"<li><a href='/{html.escape(route)}'>{html.escape(title)}</a></li>"
                for title_route in items if "=" in title_route
                for title, route in [title_route.split('=', 1)]
            )
            return f"<li><b>{key}</b>:{x_link}<ul>{links}</ul></li>"
        elif key == "css":
            return f"<li><b>{key}</b>: {value} (your selected style){x_link}</li>"
        elif key == "cookies_eaten":
            return f"<li><b>{key}</b>: {value} 🍪 (You have eaten <b>{value}</b> cookies)</li>"
        return f"<li><b>{key}</b>: {value}{x_link}</li>"

    if not cookies_ok:
        return """
        <h1>You are currently not holding any cookies from this website</h1>
        <p>Until you press the "Accept our cookies" button below, your actions
        on this site will not be recorded, but your interaction may also be limited.</p>
        <p>This restriction exists because some functionality (like navigation history,
        styling preferences, or shopping carts) depends on cookies.</p>
        <form method="POST" action="/cookies/accept" style="margin-top: 2em;">
            <button type="submit" style="font-size:1.2em; padding:0.5em 2em;">Accept our cookies</button>
        </form>
        """
    else:
        stored = []
        for key in sorted(request.cookies):
            val = get(key, "")
            stored.append(describe_cookie(key, val))

        cookies_html = "<ul>" + "".join(stored) + "</ul>" if stored else "<p>No stored cookies found.</p>"

        removal_form = """
            <form method="POST" action="/cookies/remove" style="margin-top:2em;">
                <div style="display: flex; align-items: center; margin-bottom: 1em; gap: 0.5em;">
                    <input type="checkbox" id="confirm" name="confirm" value="1" required
                        style="width:1.2em; height:1.2em; vertical-align:middle; margin:0;" />
                    <label for="confirm" style="margin:0; cursor:pointer; font-size:1em; line-height:1.2;">
                        I understand my cookie data cannot be recovered once deleted.
                    </label>
                </div>
                <button type="submit" style="color:white;background:#a00;padding:0.4em 2em;font-size:1em;border-radius:0.4em;border:none;">
                    Delete all my cookie data
                </button>
            </form>
        """

        return f"""
        <h1>Cookies are enabled for this site</h1>
        <p>Below is a list of the cookie-based information we are currently storing about you:</p>
        {cookies_html}
        <p>We never sell your data. We never share your data beyond the service providers used to host and deliver 
        this website, including database, CDN, and web infrastructure providers necessary to fulfill your requests.</p>
        <p>You can remove all stored cookie information at any time by using the form below.</p>
        {removal_form}
        <hr>
        <p>On the other hand, you can make your cookies available in other browsers and devices by configuring a mask.</p>
        <p><a href="/cookies/my-mask">Learn more about masks.</a></p>
        """

Sample CLI

gway web.cookies view-my-mask

Full Code

def view_my_mask(*, claim=None, set_mask=None):
    """
    View and manage mask linking for cookies.
    - GET: Shows current mask and allows claim or update.
    - POST (claim/set_mask): Claim a mask and save/load cookies to/from masks.cdv.
    
    If user claims an existing mask AND already has a mask cookie, 
    ALL existing cookies (except cookies_accepted) are wiped before restoring the claimed mask.
    No wipe is performed when creating a new mask.
    """
    cookies_ok = check_consent()
    mask = get("mask", "")

    # Handle claiming or setting mask via POST
    if claim or set_mask:
        ident = (claim or set_mask or "").strip()
        norm = _normalize_mask(ident)
        if not norm:
            msg = "<b>mask string is invalid.</b> Please use only letters, numbers, and dashes."
        else:
            mask_map = _read_masks()
            existing = mask_map.get(norm)
            if not existing:
                # New mask: Save all current cookies (except mask and cookies_accepted) to record
                current = _get_current_cookies()
                filtered = {k: v for k, v in current.items() if k not in ("mask", "cookies_accepted")}
                mask_map[norm] = filtered
                _write_masks(mask_map)
                set("mask", norm)
                msg = (
                    f"<b>mask <code>{html.escape(norm)}</code> claimed and stored!</b> "
                    "Your cookie data has been saved under this mask. "
                    "You may now restore it from any device or browser by claiming this mask again."
                )
            else:
                # If user already has a mask, wipe all their cookies (except cookies_accepted) before restoring
                if mask:
                    for k in list(request.cookies):
                        if k not in ("cookies_accepted",):
                            remove(k)
                # Restore cookies from mask
                _restore_cookies(existing)
                set("mask", norm)
                # Merge new cookies into record (overwriting with current, but not blanking any missing)
                merged = existing.copy()
                for k, v in _get_current_cookies().items():
                    if k not in ("mask", "cookies_accepted"):
                        merged[k] = v
                mask_map[norm] = merged
                _write_masks(mask_map)
                msg = (
                    f"<b>mask <code>{html.escape(norm)}</code> loaded!</b> "
                    "All cookies for this mask have been restored and merged with your current data. "
                    "Future changes to your cookies will update this mask."
                )
        # After processing, reload view with message
        return view_my_mask() + f"<div style='margin:1em 0; color:#080;'>{msg}</div>"

    # GET: Show info, form, and current mask
    mask_note = (
        f"<div style='margin:1em 0; color:#005;'><b>Current mask:</b> <code>{html.escape(mask)}</code></div>"
        if mask else
        "<div style='margin:1em 0; color:#888;'>You have not claimed a mask yet.</div>"
    )
    claim_form = """
    <form method="POST" style="margin-top:1em;">
        <label for="mask" style="font-size:1em;">
            Enter a mask string to claim (letters, numbers, dashes):</label>
        <input type="text" id="mask" name="set_mask" required pattern="[a-zA-Z0-9\\-]+"
               style="margin-left:0.5em; font-size:1.1em; width:12em; border-radius:0.3em; border:1px solid #aaa;"/>
        <button type="submit" style="margin-left:1em; font-size:1em;">Claim / Load</button>
    </form>
    """
    return f"""
    <h1>Cookie Masks</h1>
    <p>
        <strong>Masks</strong> allow you to copy your cookie data (such as preferences, navigation history, cart, etc)
        from one device or browser to another, without needing to register an account.
        Claiming a mask will save a copy of your current cookie data under the mask string you provide.<br>
        <b>Warning:</b> Anyone who knows this mask string can restore your cookie data, so choose carefully.
    </p>
    {mask_note}
    {claim_form}
    <p style='margin-top:2em; color:#555; font-size:0.98em;'>
        To transfer your Cookies as a Mask:<br>
        1. On your main device, claim mask (e.g. "my-handle-123").<br>
        2. On another device/browser, visit this page and claim the same mask to restore your data.<br>
        3. Any changes you make while holding a mask will update the stored copy.
    </p>
    """

Sample CLI

gway web.cookies view-remove

Full Code

def view_remove(*, next="/cookies/cookie-jar", confirm = False):
    # Only proceed if the confirmation checkbox was passed in the form
    if not confirm:
        response.status = 303
        response.set_header("Location", next)
        return ""
    if not check_consent():
        response.status = 303
        response.set_header("Location", next)
        return ""
    clear_all()
    response.status = 303
    response.set_header("Location", next)
    return ""

Sample CLI

gway web.error redirect

References

Full Code

def redirect(message="", *, err=None, default=None, view_name=None):
    """
    GWAY error/redirect handler.
    Deprecated: 'view_name'. Now uses gw.web.app.current_endpoint.
    """
    from bottle import request, response

    debug_enabled = bool(getattr(gw, "debug", False))
    visited = gw.web.cookies.get("visited", "")
    visited_items = visited.split("|") if visited else []

    # --- DEPRECATED: view_name, use gw.web.app.current_endpoint instead ---
    if view_name is not None:
        import warnings
        warnings.warn(
            "redirect(): 'view_name' is deprecated. Use gw.web.app.current_endpoint instead.",
            DeprecationWarning
        )
    curr_view = getattr(gw.web.app, "current_endpoint", None)
    view_key = curr_view() if callable(curr_view) else curr_view
    if not view_key and view_name:
        view_key = view_name

    pruned = False
    if view_key and gw.web.cookies.check_consent():
        norm_broken = (view_key or "").replace("-", " ").replace("_", " ").title().lower()
        new_items = []
        for v in visited_items:
            title = v.split("=", 1)[0].strip().lower()
            if title == norm_broken:
                pruned = True
                continue
            new_items.append(v)
        if pruned:
            gw.web.cookies.set("visited", "|".join(new_items))
            visited_items = new_items

    if debug_enabled:
        return view_debug_error(
            title="GWAY Debug Error",
            message=message,
            err=err,
            status=500,
            default=default
        )

    response.status = 302
    response.set_header("Location", default or gw.web.app.default_home())
    return ""

Sample CLI

gway web.nav get-style

References

Full Code

def get_style():
    """
    Returns the current user's preferred style path (to .css file), checking:
    - URL ?css= param (for preview/testing)
    - 'css' cookie
    - First available style, or '/static/styles/base.css' if none found
    This should be called by render_template for every page load.
    """
    styles = list_styles()
    style_cookie = gw.web.cookies.get("css") if gw.web.app.is_enabled('web.cookies') else None
    style_query = request.query.get("css")
    style_path = None

    # Prefer query param (if exists and valid)
    if style_query:
        for src, fname in styles:
            if fname == style_query:
                style_path = f"/static/styles/{fname}" if src == "global" else f"/static/{src}/styles/{fname}"
                break
    # Otherwise, prefer cookie
    if not style_path and style_cookie:
        for src, fname in styles:
            if fname == style_cookie:
                style_path = f"/static/styles/{fname}" if src == "global" else f"/static/{src}/styles/{fname}"
                break
    # Otherwise, first available style
    if not style_path and styles:
        src, fname = styles[0]
        style_path = f"/static/styles/{fname}" if src == "global" else f"/static/{src}/styles/{fname}"
    # Fallback to base
    return style_path or "/static/styles/base.css"

Sample CLI

gway web.nav render

References

Full Code

def render(*, homes=None):
    """
    Renders the sidebar navigation including search, home links, visited links, and a QR compass.
    """
    cookies_ok = gw.web.app.is_enabled('web.cookies') and gw.web.cookies.check_consent()
    gw.debug(f"Render nav with {homes=} {cookies_ok=}")

    visited = []
    if cookies_ok:
        visited_cookie = gw.web.cookies.get("visited", "")
        if visited_cookie:
            visited = visited_cookie.split("|")

    current_route = request.fullpath.strip("/")
    current_title = (current_route.split("/")[-1] or "readme").replace('-', ' ').replace('_', ' ').title()

    visited_set = set()
    entries = []
    for entry in visited:
        if "=" not in entry:
            continue
        title, route = entry.split("=", 1)
        canon_route = route.strip("/")
        if canon_route not in visited_set:
            entries.append((title, canon_route))
            visited_set.add(canon_route)

    home_routes = set()
    if homes:
        for home_title, home_route in homes:
            home_routes.add(home_route.strip("/"))
    if cookies_ok and current_route not in home_routes and current_route not in visited_set:
        entries.append((current_title, current_route))
        visited_set.add(current_route)

    # --- Build HTML links ---
    links = ""
    if homes:
        for home_title, home_route in homes:
            route = home_route.strip("/")
            is_current = ' class="current"' if route == current_route else ""
            links += f'<li><a href="/{home_route}"{is_current}>{home_title.upper()}</a></li>'
    if cookies_ok and entries:
        visited_rendered = set()
        for title, route in reversed(entries):
            if route in home_routes or route in visited_rendered:
                continue
            visited_rendered.add(route)
            is_current = ' class="current"' if route == current_route else ""
            links += f'<li><a href="/{route}"{is_current}>{title}</a></li>'
    elif not homes:
        links += f'<li class="current">{current_title.upper()}</li>'

    # --- Search box ---
    search_box = '''
        <form action="/site/help" method="get" class="nav">
            <textarea name="topic" id="help-search"
                placeholder="Search this GWAY"
                class="help" rows="1"
                autocomplete="off"
                spellcheck="false"
                style="overflow:hidden; resize:none; min-height:2.4em; max-height:10em;"
                oninput="autoExpand(this)"
            >{}</textarea>
        </form>
    '''.format(request.query.get("topic", ""))

    # --- QR code for this page ---
    compass = ""
    try:
        url = get_current_url()
        qr_url = gw.qr.generate_url(url)
        compass = f'''
            <div class="compass">
                <img src="{qr_url}" alt="QR Code" class="compass" />
            </div>
        '''
    except Exception as e:
        gw.debug(f"Could not generate QR compass: {e}")

    gw.debug(f"Visited cookie raw: {gw.web.cookies.get('visited')}")
    return f"<aside>{search_box}<ul>{links}</ul><br>{compass}</aside>"

Sample CLI

gway web.nav view-style-switcher

References

Full Code

def view_style_switcher(*, css=None, project=None):
    """
    Shows available styles (global + project), lets user choose, preview, and see raw CSS.
    If cookies are accepted, sets the style via cookie when changed in dropdown.
    If cookies are not accepted, only uses the css param for preview.
    """
    import os
    from bottle import request, response

    # Determine the project from context or fallback if not provided
    if not project:
        path = request.fullpath.strip("/").split("/")
        if path and path[0] in ("conway", "awg", "site", "etron"):
            project = path[0]
        else:
            project = "site"

    def list_styles_local(project):
        seen = set()
        styles = []
        # Global styles
        global_dir = gw.resource("data", "static", "styles")
        if os.path.isdir(global_dir):
            for f in sorted(os.listdir(global_dir)):
                if f.endswith(".css") and os.path.isfile(os.path.join(global_dir, f)):
                    if f not in seen:
                        styles.append(("global", f))
                        seen.add(f)
        if project:
            proj_dir = gw.resource("data", "static", project, "styles")
            if os.path.isdir(proj_dir):
                for f in sorted(os.listdir(proj_dir)):
                    if f.endswith(".css") and os.path.isfile(os.path.join(proj_dir, f)):
                        if f not in seen:
                            styles.append((project, f))
                            seen.add(f)
        return styles

    styles = list_styles_local(project)
    all_styles = [fname for _, fname in styles]
    style_sources = {fname: src for src, fname in styles}

    cookies_enabled = gw.web.app.is_enabled('web.cookies')
    cookies_accepted = gw.web.cookies.check_consent() if cookies_enabled else False
    css_cookie = gw.web.cookies.get("css")

    # Handle POST
    if request.method == "POST":
        selected_style = request.forms.get("css")
        if cookies_enabled and cookies_accepted and selected_style and selected_style in all_styles:
            gw.web.cookies.set("css", selected_style)
            response.status = 303
            response.set_header("Location", request.fullpath)
            return ""

    # --- THIS IS THE MAIN LOGIC: ---
    # Priority: query param > explicit function arg > cookie > default
    style_query = request.query.get("css")
    selected_style = (
        style_query if style_query in all_styles else
        (css if css in all_styles else
         (css_cookie if css_cookie in all_styles else
          (all_styles[0] if all_styles else "base.css")))
    )
    # If still not valid, fallback to default
    if selected_style not in all_styles:
        selected_style = all_styles[0] if all_styles else "base.css"

    # Determine preview link and path for raw CSS
    if style_sources.get(selected_style) == "global":
        preview_href = f"/static/styles/{selected_style}"
        css_path = gw.resource("data", "static", "styles", selected_style)
        css_link = f'<link rel="stylesheet" href="/static/styles/{selected_style}">'
    else:
        preview_href = f"/static/{project}/styles/{selected_style}"
        css_path = gw.resource("data", "static", project, "styles", selected_style)
        css_link = f'<link rel="stylesheet" href="/static/{project}/styles/{selected_style}">'

    preview_html = f"""
        {css_link}
        <div class="style-preview">
            <h2>Theme Preview: {selected_style[:-4].replace('_', ' ').title()}</h2>
            <p>This is a preview of the <b>{selected_style}</b> theme.</p>
            <button>Sample button</button>
            <pre>code block</pre>
        </div>
    """
    css_code = ""
    try:
        with open(css_path, encoding="utf-8") as f:
            css_code = f.read()
    except Exception:
        css_code = "Could not load CSS file."

    selector = style_selector_form(
        all_styles=styles,
        selected_style=selected_style,
        cookies_enabled=cookies_enabled,
        cookies_accepted=cookies_accepted,
        project=project
    )

    return f"""
        <h1>Select a Site Theme</h1>
        {selector}
        {preview_html}
        <h3>CSS Source: {selected_style}</h3>
        <pre style="max-height:400px;overflow:auto;">{html_escape(css_code)}</pre>
    """