<?php
// jabali-sso-__JABALI_NONCE__.php — auto-generated by jabali-agent for Drupal install __JABALI_INSTALL_ID__.
// Self-deleting single-use admin login. See ADR-0040.
//
// Approach: rather than programmatically calling user_login_finalize()
// (which doesn't reliably establish a session + cookie outside the normal
// kernel->handle()->terminate() request lifecycle), we generate Drupal's
// native one-time-login URL (/user/reset/<uid>/<ts>/<hash>/login) and
// redirect the browser to it. Drupal's UserController::resetPassLogin
// then handles hash validation, session init, auth cookie, and final
// redirect via its standard request pipeline. This is the same mechanism
// `drush user:login` uses and it is battle-tested across Drupal versions.

declare(strict_types=1);

// 0. Capture any fatal error that happens below into the webserver log
//    so the operator has something to look at when an SSO attempt 500s.
register_shutdown_function(function (): void {
    $err = error_get_last();
    if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) {
        error_log('jabali-sso drupal FATAL install __JABALI_INSTALL_ID__: ' . $err['message'] . ' at ' . $err['file'] . ':' . $err['line']);
    }
});

// 1. Reject speculative navigation (prefetch / prerender / preview) and
//    non-GET methods before consuming any single-use state. See ADR-0040
//    and sso_template_wordpress.php for the full rationale.
$secPurpose = $_SERVER['HTTP_SEC_PURPOSE'] ?? '';
$purpose    = $_SERVER['HTTP_PURPOSE']     ?? '';
$xMoz       = $_SERVER['HTTP_X_MOZ']       ?? '';
$xPurpose   = $_SERVER['HTTP_X_PURPOSE']   ?? '';
if ($secPurpose !== ''
 || $purpose === 'prefetch'
 || stripos($xMoz, 'prefetch') !== false
 || $xPurpose === 'preview'
 || ($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') {
    http_response_code(204);
    exit;
}

// 2. Atomic lock claim. Second concurrent open fails LOCK_NB and exits
//    without side effects. unlink alone is insufficient because PHP
//    keeps the inode alive on open file handles — flock closes the race.
$lock = @fopen(__FILE__, 'r');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
    http_response_code(409);
    exit;
}

// 3. Inline TTL check (defence in depth — systemd reaper sweeps every 30s
//    so worst-case stranded-file lifetime is 60+30 = 90s).
if (filemtime(__FILE__) + __JABALI_TTL_SECONDS__ < time()) {
    http_response_code(410);
    exit;
}

// 4. Bootstrap Drupal. autoload.php lives at the install root; the agent
//    bakes its absolute path in at write time. Wrap in try/catch so a
//    broken settings.php or missing dependency surfaces as a loggable
//    500 instead of a blank screen.
try {
    $autoloader = require_once __JABALI_AUTOLOAD_PATH__;
    $request    = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
    $kernel     = \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod');
    $kernel->boot();
    $kernel->preHandle($request);
} catch (\Throwable $e) {
    error_log('jabali-sso drupal bootstrap failed install __JABALI_INSTALL_ID__: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
    http_response_code(500);
    exit;
}

// 5. Resolve the admin user by username. user_load_by_name is the
//    procedural helper; mirrors what /user/login does.
$admin_username = __JABALI_ADMIN_USERNAME__;
$account = user_load_by_name($admin_username);
if (!$account || !$account->isActive()) {
    error_log('jabali-sso: drupal admin lookup failed for install __JABALI_INSTALL_ID__');
    http_response_code(404);
    exit;
}

// 6. Build Drupal's native one-time-login URL. user_pass_rehash is the
//    same function core's password-reset flow uses; the hash is bound to
//    the user's current password hash + last-login-time, so it becomes
//    invalid as soon as the operator logs in (effectively single-use).
//
//    DO NOT use Url::fromRoute here. Drupal's UrlGenerator derives URLs
//    from the *current* request's script-path; for the SSO request that
//    script-path is "/<subdir>/jabali-sso-<nonce>.php", so the generated
//    URL becomes "/<subdir>/jabali-sso-<nonce>.php/user/reset/..." and
//    the browser hits 404 because the SSO file has already been
//    unlinked. Construct the path by hand against $_SERVER['SCRIPT_NAME']
//    instead — the route shape (/user/reset/<uid>/<ts>/<hash>/login)
//    has been stable across every supported Drupal version.
try {
    $timestamp = \Drupal::time()->getRequestTime();
    $hash      = user_pass_rehash($account, $timestamp);

    $subdir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/'), '/\\');
    if ($subdir === '\\' || $subdir === '/') {
        $subdir = '';
    }

    $destination = ($subdir === '' ? '' : $subdir) . '/admin';
    $loginUrl    = sprintf(
        '%s/user/reset/%s/%d/%s/login?destination=%s',
        $subdir,
        rawurlencode((string) $account->id()),
        $timestamp,
        rawurlencode($hash),
        rawurlencode($destination)
    );
} catch (\Throwable $e) {
    error_log('jabali-sso drupal URL build failed install __JABALI_INSTALL_ID__: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
    http_response_code(500);
    exit;
}

// 7. Audit on the Drupal side. Don't log the uid/username.
error_log('jabali-sso: drupal one-time login URL issued for install __JABALI_INSTALL_ID__');

// 8. Don't leak the filename via Referer to Drupal's downstream assets.
header('Referrer-Policy: no-referrer');

// 9. Unlink LAST — after the URL is built. Transient failure between
//    here and the redirect leaves the file on disk for a retry; the
//    reaper sweeps stranded files within 90s either way.
@unlink(__FILE__);

// 10. Redirect to Drupal's reset/login handler. UserController::resetPassLogin
//     validates the hash, logs the operator in via its own request pipeline
//     (sets session + auth cookie correctly), then redirects to the
//     destination query param (/admin).
header('Location: ' . $loginUrl);
exit;
