<?php
/**
 * Site Remedy AI Emergency Recovery Bridge.
 * This file is copied outside the WordPress plugin directory when explicitly enabled.
 * It does NOT bootstrap WordPress, so it can remain usable when wp-admin is failing.
 */
define( 'XWPCP_BRIDGE_RUNTIME', true );
header( 'X-Robots-Tag: noindex, nofollow, noarchive', true );
header( 'X-Frame-Options: DENY', true );
header( 'Referrer-Policy: no-referrer', true );
header( 'X-Content-Type-Options: nosniff', true );
header( 'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()', true );
header( "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", true );
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true );

$base = __DIR__;
$config_file = $base . '/config.php';
$snapshot_file = $base . '/snapshot.php';
$map_file = $base . '/disabled-map.php';
$attempts_file = $base . '/attempts.php';
$actions_file = $base . '/actions.php';
if ( ! is_file( $config_file ) || ! is_file( $snapshot_file ) ) { http_response_code(503); exit( 'Site Remedy recovery bridge is not configured.' ); }
$config = require $config_file;
$snapshot = require $snapshot_file;
$disabled_map = is_file( $map_file ) ? require $map_file : array();
$attempts = is_file( $attempts_file ) ? require $attempts_file : array();
$action_journal = is_file( $actions_file ) ? require $actions_file : array();
if ( ! is_array( $action_journal ) ) $action_journal = array();
if ( ! is_array( $config ) || empty( $config['token_hash'] ) || ! is_array( $snapshot ) ) { http_response_code(503); exit( 'Site Remedy recovery bridge configuration is invalid.' ); }

function xwpcp_bridge_write( $path, $data ) {
    $content = "<?php\nif ( ! defined( 'XWPCP_BRIDGE_RUNTIME' ) ) { http_response_code(404); exit; }\nreturn " . var_export( $data, true ) . ";\n";
    $ok = false !== @file_put_contents( $path, $content, LOCK_EX );
    if ( $ok ) @chmod( $path, 0600 );
    return $ok;
}
function xwpcp_bridge_local_request() {
    $host = isset( $_SERVER['HTTP_HOST'] ) ? strtolower( preg_replace( '/:\\d+$/', '', $_SERVER['HTTP_HOST'] ) ) : '';
    $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
    return in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) || in_array( $ip, array( '127.0.0.1', '::1' ), true );
}
function xwpcp_bridge_https() {
    return ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== strtolower( (string) $_SERVER['HTTPS'] ) ) || ( isset( $_SERVER['SERVER_PORT'] ) && '443' === (string) $_SERVER['SERVER_PORT'] );
}
function xwpcp_bridge_safe_plugin_target( $plugin_root, $plugin_file ) {
    $plugin_file = str_replace( '\\', '/', (string) $plugin_file );
    if ( '' === $plugin_file || false !== strpos( $plugin_file, '..' ) || '/' === substr( $plugin_file, 0, 1 ) ) return false;
    $parts = explode( '/', $plugin_file );
    if ( count( $parts ) > 1 ) $target = rtrim( $plugin_root, '/\\' ) . DIRECTORY_SEPARATOR . $parts[0];
    else $target = rtrim( $plugin_root, '/\\' ) . DIRECTORY_SEPARATOR . $plugin_file;
    $root = realpath( $plugin_root );
    $real = realpath( $target );
    if ( false === $root || false === $real || 0 !== strpos( str_replace( '\\', '/', $real ), rtrim( str_replace( '\\', '/', $root ), '/' ) . '/' ) ) return false;
    if ( is_link( $real ) ) return false;
    return $real;
}
function xwpcp_bridge_find_plugin( $snapshot, $file ) {
    foreach ( isset( $snapshot['plugins'] ) && is_array( $snapshot['plugins'] ) ? $snapshot['plugins'] : array() as $plugin ) {
        if ( isset( $plugin['file'] ) && hash_equals( (string) $plugin['file'], (string) $file ) ) return $plugin;
    }
    return null;
}

$secure = xwpcp_bridge_https() || xwpcp_bridge_local_request();
if ( ! $secure ) { http_response_code(403); exit( 'Site Remedy Recovery Bridge requires HTTPS outside localhost.' ); }

$cookie_secure = xwpcp_bridge_https();
if ( PHP_VERSION_ID >= 70300 ) session_set_cookie_params( array( 'lifetime' => 0, 'path' => '/', 'secure' => $cookie_secure, 'httponly' => true, 'samesite' => 'Strict' ) );
else session_set_cookie_params( 0, '/; samesite=Strict', '', $cookie_secure, true );
session_name( 'xwpcp_recovery' );
session_start();
if ( empty( $_SESSION['csrf'] ) ) $_SESSION['csrf'] = bin2hex( random_bytes( 16 ) );

$ip_key = hash( 'sha256', isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown' );
$now = time();
foreach ( $attempts as $k => $entry ) if ( ! is_array( $entry ) || empty( $entry['until'] ) || $entry['until'] < $now - 3600 ) unset( $attempts[$k] );
$entry = isset( $attempts[$ip_key] ) && is_array( $attempts[$ip_key] ) ? $attempts[$ip_key] : array( 'count' => 0, 'until' => 0 );
$error = '';
$message = '';

if ( isset( $_POST['action'] ) && 'login' === $_POST['action'] ) {
    $login_csrf = isset( $_POST['csrf'] ) ? (string) $_POST['csrf'] : '';
    if ( empty( $_SESSION['csrf'] ) || ! hash_equals( $_SESSION['csrf'], $login_csrf ) ) {
        http_response_code( 403 );
        exit( 'Invalid request token.' );
    }
    if ( ! empty( $entry['until'] ) && $entry['until'] > $now ) {
        $error = 'Too many failed attempts. Try again later.';
    } else {
        $token = isset( $_POST['token'] ) ? trim( (string) $_POST['token'] ) : '';
        if ( password_verify( $token, (string) $config['token_hash'] ) ) {
            session_regenerate_id( true );
            $_SESSION['authenticated'] = true;
            $_SESSION['authenticated_at'] = $now;
            $_SESSION['csrf'] = bin2hex( random_bytes( 16 ) );
            $attempts[$ip_key] = array( 'count' => 0, 'until' => 0 );
            xwpcp_bridge_write( $attempts_file, $attempts );
            header( 'Location: ./', true, 303 ); exit;
        }
        $entry['count'] = (int) $entry['count'] + 1;
        if ( $entry['count'] >= 5 ) $entry['until'] = $now + 15 * 60;
        $attempts[$ip_key] = $entry;
        xwpcp_bridge_write( $attempts_file, $attempts );
        $error = 'Invalid recovery key.';
    }
}

if ( isset( $_POST['action'] ) && 'logout' === $_POST['action'] ) {
    $logout_csrf = isset( $_POST['csrf'] ) ? (string) $_POST['csrf'] : '';
    if ( empty( $_SESSION['csrf'] ) || ! hash_equals( $_SESSION['csrf'], $logout_csrf ) ) { http_response_code(403); exit( 'Invalid request token.' ); }
    $_SESSION = array(); session_destroy(); header( 'Location: ./', true, 303 ); exit;
}

$authenticated = ! empty( $_SESSION['authenticated'] );
if ( $authenticated && ! empty( $_SESSION['authenticated_at'] ) && $now - (int) $_SESSION['authenticated_at'] > 30 * 60 ) {
    $_SESSION = array(); session_destroy(); $authenticated = false;
}

if ( $authenticated && isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'disable_plugin', 'restore_plugin' ), true ) ) {
    $csrf = isset( $_POST['csrf'] ) ? (string) $_POST['csrf'] : '';
    if ( empty( $_SESSION['csrf'] ) || ! hash_equals( $_SESSION['csrf'], $csrf ) ) { http_response_code(403); exit( 'Invalid request token.' ); }
    $file = isset( $_POST['plugin'] ) ? (string) $_POST['plugin'] : '';
    $plugin = xwpcp_bridge_find_plugin( $snapshot, $file );
    if ( ! $plugin ) $error = 'Plugin is not present in Site Remedy\'s last-known snapshot.';
    elseif ( false !== strpos( $file, 'site-remedy-ai' ) ) $error = 'Site Remedy does not allow the bridge to disable itself.';
    else {
        $root = isset( $config['plugin_root'] ) ? (string) $config['plugin_root'] : '';
        $target = xwpcp_bridge_safe_plugin_target( $root, $file );
        if ( 'disable_plugin' === $_POST['action'] ) {
            if ( ! $target || ! file_exists( $target ) ) $error = 'Plugin target could not be validated.';
            elseif ( empty( $plugin['active'] ) ) $error = 'Site Remedy\'s last-known snapshot does not mark this plugin active.';
            else {
                $disabled = $target . '.carepilot-disabled-' . gmdate( 'Ymd-His' );
                if ( @rename( $target, $disabled ) ) {
                    $disabled_map[$file] = array( 'original' => $target, 'disabled' => $disabled, 'time' => $now, 'name' => isset( $plugin['name'] ) ? $plugin['name'] : $file );
                    xwpcp_bridge_write( $map_file, $disabled_map );
                    $action_journal[] = array(
                        'id' => bin2hex( random_bytes( 12 ) ),
                        'action' => 'disable_plugin',
                        'plugin' => $file,
                        'name' => isset( $plugin['name'] ) ? $plugin['name'] : $file,
                        'time' => $now,
                        'source' => 'action_journal',
                    );
                    xwpcp_bridge_write( $actions_file, array_slice( $action_journal, -100 ) );
                    $message = 'Plugin files were safely disabled. Reload the site or wp-admin to test recovery. Site Remedy will import this action into recovery history after WordPress boots.';
                } else $error = 'Site Remedy could not rename the plugin target.';
            }
        } else {
            if ( empty( $disabled_map[$file]['original'] ) || empty( $disabled_map[$file]['disabled'] ) ) $error = 'No bridge rollback record exists for this plugin.';
            else {
                $original = $disabled_map[$file]['original']; $disabled = $disabled_map[$file]['disabled'];
                if ( file_exists( $original ) ) $error = 'Original plugin path already exists; rollback was not attempted.';
                elseif ( ! file_exists( $disabled ) ) $error = 'Disabled plugin path no longer exists.';
                elseif ( @rename( $disabled, $original ) ) {
                    $name = isset( $disabled_map[$file]['name'] ) ? $disabled_map[$file]['name'] : $file;
                    unset( $disabled_map[$file] );
                    xwpcp_bridge_write( $map_file, $disabled_map );
                    $action_journal[] = array(
                        'id' => bin2hex( random_bytes( 12 ) ),
                        'action' => 'restore_plugin',
                        'plugin' => $file,
                        'name' => $name,
                        'time' => $now,
                        'source' => 'action_journal',
                    );
                    xwpcp_bridge_write( $actions_file, array_slice( $action_journal, -100 ) );
                    $message = 'Plugin files were restored. Site Remedy will import this rollback into recovery history after WordPress boots. Normal plugin activation may still be required.';
                }
                else $error = 'Site Remedy could not restore the plugin files.';
            }
        }
    }
}

function h( $v ) { return htmlspecialchars( (string) $v, ENT_QUOTES, 'UTF-8' ); }
?><!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Site Remedy Emergency Recovery</title><style>
body{margin:0;background:#f4f6f8;color:#1d2327;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.wrap{max-width:980px;margin:40px auto;padding:0 20px}.card{background:#fff;border:1px solid #dcdcde;border-radius:12px;padding:24px;margin:0 0 18px;box-shadow:0 1px 2px rgba(0,0,0,.03)}h1{margin:0 0 8px;font-size:28px}h2{font-size:18px;margin:0 0 14px}.muted{color:#646970}.ok{color:#008a20}.warn{color:#b26200}.bad{color:#b32d2e}.notice{padding:12px 14px;border-radius:8px;margin:0 0 16px}.notice.bad{background:#fcf0f1}.notice.ok{background:#edfaef}input[type=password]{width:100%;max-width:520px;padding:12px;border:1px solid #8c8f94;border-radius:6px}.btn{display:inline-block;border:1px solid #3858e9;background:#3858e9;color:#fff;padding:9px 14px;border-radius:5px;text-decoration:none;cursor:pointer;font-weight:600}.btn.secondary{background:#fff;color:#3858e9}.btn.danger{background:#b32d2e;border-color:#b32d2e}.row{display:flex;justify-content:space-between;gap:20px;align-items:center;border-top:1px solid #eee;padding:14px 0}.row:first-child{border-top:0}.meta{display:flex;gap:18px;flex-wrap:wrap}.pill{background:#f0f0f1;border-radius:999px;padding:4px 9px;font-size:12px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}form{margin:0}.small{font-size:12px}.footer{color:#646970;font-size:12px;text-align:center;padding:8px 0 28px}@media(max-width:700px){.row{align-items:flex-start;flex-direction:column}.actions{width:100%}}
</style></head><body><div class="wrap">
<div class="card"><h1>Site Remedy Emergency Recovery</h1><p class="muted">Standalone local recovery bridge. WordPress is not bootstrapped on this page.</p></div>
<?php if ( $error ): ?><div class="notice bad"><?php echo h($error); ?></div><?php endif; ?><?php if ( $message ): ?><div class="notice ok"><?php echo h($message); ?></div><?php endif; ?>
<?php if ( ! $authenticated ): ?>
<div class="card"><h2>Authenticate</h2><p>Enter the recovery key generated from Site Remedy Settings.</p><form method="post"><input type="hidden" name="action" value="login"><input type="hidden" name="csrf" value="<?php echo h($_SESSION['csrf']); ?>"><p><input type="password" name="token" autocomplete="off" required></p><button class="btn" type="submit">Open recovery console</button></form><p class="small muted">Five failed attempts temporarily lock this client for 15 minutes.</p></div>
<?php else: ?>
<div class="card"><div class="row"><div><h2>Last-known site state</h2><div class="meta"><span class="pill">WordPress <?php echo h(isset($snapshot['wp_version'])?$snapshot['wp_version']:'—'); ?></span><span class="pill">PHP <?php echo h(isset($snapshot['php_version'])?$snapshot['php_version']:'—'); ?></span><?php if(isset($snapshot['health_score'])&&null!==$snapshot['health_score']):?><span class="pill">Health <?php echo h($snapshot['health_score']); ?>/100</span><?php endif;?></div><p class="small muted">Snapshot: <?php echo !empty($snapshot['generated_at'])?h(date('Y-m-d H:i:s',(int)$snapshot['generated_at'])):'unknown'; ?></p></div><form method="post"><input type="hidden" name="action" value="logout"><input type="hidden" name="csrf" value="<?php echo h($_SESSION['csrf']); ?>"><button class="btn secondary">Log out</button></form></div></div>
<div class="card"><h2>Plugin recovery</h2><p class="muted">Only plugins from Site Remedy's last-known snapshot can be changed. Site Remedy itself is protected. Actions rename plugin files; they do not edit the database.</p>
<?php foreach ( isset($snapshot['plugins'])&&is_array($snapshot['plugins'])?$snapshot['plugins']:array() as $p ): $file=isset($p['file'])?$p['file']:''; $is_cp=false!==strpos($file,'site-remedy-ai'); $mapped=isset($disabled_map[$file]); ?>
<div class="row"><div><strong><?php echo h(isset($p['name'])?$p['name']:$file); ?></strong><div class="small muted"><?php echo h($file); ?><?php if(!empty($p['version'])):?> · v<?php echo h($p['version']); ?><?php endif;?></div></div><div class="actions"><span class="pill"><?php echo $mapped ? 'Files disabled by bridge' : (!empty($p['active'])?'Last known active':'Last known inactive'); ?></span><?php if($mapped):?><form method="post" onsubmit="return confirm('Restore these plugin files? This can reintroduce the original failure.');"><input type="hidden" name="action" value="restore_plugin"><input type="hidden" name="plugin" value="<?php echo h($file); ?>"><input type="hidden" name="csrf" value="<?php echo h($_SESSION['csrf']); ?>"><button class="btn secondary" type="submit">Restore files</button></form><?php elseif(!$is_cp&&!empty($p['active'])):?><form method="post" onsubmit="return confirm('Temporarily disable this plugin by renaming its files?');"><input type="hidden" name="action" value="disable_plugin"><input type="hidden" name="plugin" value="<?php echo h($file); ?>"><input type="hidden" name="csrf" value="<?php echo h($_SESSION['csrf']); ?>"><button class="btn danger" type="submit">Disable plugin files</button></form><?php endif;?></div></div>
<?php endforeach; ?></div>
<div class="card"><h2>Safety boundary</h2><p>This bridge is intended for cases where the web server and PHP still run but WordPress or wp-admin cannot complete bootstrapping. It cannot repair web-server outages, PHP service failures, DNS problems, disk failures, or database-server outages by itself.</p><p class="small muted">For non-localhost use, HTTPS is required. Disable the bridge from Site Remedy Settings when you do not need emergency access.</p></div>
<?php endif; ?><div class="footer">Site Remedy AI Emergency Recovery Bridge</div></div></body></html>
