htmx/redirect
< >

return function () {
    $self = url('/@object/@action/' . urlencode('htmx/redirect'));
    // Where the button may send the browser: a fixed list, so the request can never choose a foreign URL (open redirect)
    $targets = [
        'here' => ['This demo again', $self . '?redirected=1'],
        'install' => ['The Install page', url('/@object/install')],
        'table' => ['The htmx table demo', url('/@object/@action/' . urlencode('htmx/table'))],
    ];
    $htmx = _class('htmx');
    // One URL, two answers (fragment or page): keep caches from mixing them up
    header('Vary: HX-Request, HX-History-Restore-Request');
    if ($htmx->_wants_fragment() && is_post()) {
        $to = (is_string($_POST['to'] ?? null) ? $_POST['to'] : '');
        if ( ! _class('csrf_guard')->check() || ! isset($targets[$to])) {
            return $htmx->_result('<div class="alert alert-danger">Refused: unknown target or missing CSRF token.</div>');
        }
        // sends "HX-Redirect: <url>" and ends the request; htmx then loads that URL as a full page
        return $htmx->_redirect($targets[$to][1]);
    }

    asset('htmx');
    $intro = '<p class="docs-demo-intro">A server-driven redirect for htmx: an htmx request cannot follow a normal 302 with a full page
        (htmx would swap the new page into the target), so the module answers with the <code>HX-Redirect</code> header from
        <code>_class(\'htmx\')->_redirect($url)</code> and htmx navigates the whole window. The target comes from a fixed list, never from
        the request. Other helpers: <code>_location()</code> (<code>HX-Location</code>, an htmx navigation without a reload) and
        <code>_refresh()</code> (<code>HX-Refresh</code>). Classes: <code>yf_htmx</code>, <code>yf_csrf_guard</code>.</p>';
    $done = isset($_GET['redirected']) ? '<div class="alert alert-success">You arrived here through <code>HX-Redirect</code>.</div>' : '';
    $options = '';
    foreach ($targets as $name => $info) {
        $options .= '<option value="' . $name . '">' . _prepare_html($info[0], 0) . '</option>';
    }
    $headers = htmlspecialchars(json_encode(['X-CSRF-Token' => _class('csrf_guard')->session_token()]), ENT_QUOTES);
    return $intro . $done
        . '<form class="form-inline docs-demo-htmx-redirect" hx-post="' . $self . '" hx-target="#hx-redirect-result" hx-headers="' . $headers . '">'
        . '<select name="to" class="form-control form-select">' . $options . '</select> '
        . '<button type="submit" class="btn btn-primary">Go (HX-Redirect)</button> '
        . '<button type="submit" name="to" value="evil" class="btn btn-default btn-outline-secondary">Try a target that is not on the list</button>'
        . '</form><div id="hx-redirect-result" class="docs-demo-result-box"></div>';
};

A server-driven redirect for htmx: an htmx request cannot follow a normal 302 with a full page (htmx would swap the new page into the target), so the module answers with the HX-Redirect header from _class('htmx')->_redirect($url) and htmx navigates the whole window. The target comes from a fixed list, never from the request. Other helpers: _location() (HX-Location, an htmx navigation without a reload) and _refresh() (HX-Refresh). Classes: yf_htmx, yf_csrf_guard.