<?php
/** Kurzlebige Einmal-Links für den sicheren Wechsel in das WordPress-Backend. */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

define( 'WPAGENT_COMPANION_BROWSER_LINK_PREFIX', 'wp_agent_browser_link_' );
define( 'WPAGENT_COMPANION_BROWSER_LINK_TTL', 120 );
define( 'WPAGENT_COMPANION_BROWSER_LINK_HEADER', 'X-WPAgent-Browser-Token' );

/** @return array<string,string> */
function wpagent_companion_browser_destinations(): array {
	return array(
		'dashboard' => 'index.php',
		'posts'     => 'edit.php',
		'pages'     => 'edit.php?post_type=page',
		'media'     => 'upload.php',
	);
}

/** @return array<string,mixed> */
function wpagent_ability_create_browser_link(): array {
	return array(
		'label'               => __( 'Backend-Einmal-Link erstellen', 'wp-agent-companion' ),
		'description'         => __( 'Erstellt für den aktuell authentifizierten, sicheren Nicht-Administrator mit Inhaltsrechten einen kurzlebigen, einmal verwendbaren Link ins WordPress-Backend. Das Browserwerkzeug muss die zurückgegebenen Header beim Aufruf der URL mitsenden.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'properties'           => array(
				'destination' => array( 'type' => 'string', 'maxLength' => 16, 'enum' => array_keys( wpagent_companion_browser_destinations() ), 'default' => 'dashboard' ),
			),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'properties'           => array(
				'url' => array( 'type' => 'string', 'maxLength' => 2000 ), 'expires_at' => array( 'type' => 'string', 'maxLength' => 40 ),
				'user_id' => array( 'type' => 'integer' ), 'destination' => array( 'type' => 'string', 'maxLength' => 16 ),
				'one_time' => array( 'type' => 'boolean' ),
				'headers' => array(
					'type'                 => 'object',
					'properties'           => array( WPAGENT_COMPANION_BROWSER_LINK_HEADER => array( 'type' => 'string', 'minLength' => 43, 'maxLength' => 43, 'pattern' => '^[A-Za-z0-9_-]{43}$' ) ),
					'required'             => array( WPAGENT_COMPANION_BROWSER_LINK_HEADER ),
					'additionalProperties' => false,
				),
			),
			'required'             => array( 'url', 'expires_at', 'user_id', 'destination', 'one_time', 'headers' ),
		),
		'execute_callback'    => 'wpagent_exec_create_browser_link',
		'permission_callback' => 'wpagent_perm_browser_link',
		'meta'                => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ) ),
	);
}

/** @return array<string,mixed> */
function wpagent_ability_revoke_browser_link(): array {
	return array(
		'label'               => __( 'Backend-Einmal-Link widerrufen', 'wp-agent-companion' ),
		'description'         => __( 'Widerruft den noch offenen Backend-Einmal-Link des aktuell authentifizierten, sicheren Nicht-Administrators.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array( 'type' => 'object', 'properties' => array(), 'additionalProperties' => false, 'default' => array() ),
		'output_schema'       => array( 'type' => 'object', 'properties' => array( 'revoked' => array( 'type' => 'boolean' ) ), 'required' => array( 'revoked' ), 'additionalProperties' => false ),
		'execute_callback'    => 'wpagent_exec_revoke_browser_link',
		'permission_callback' => 'wpagent_perm_browser_link',
		'meta'                => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ) ),
	);
}

function wpagent_perm_browser_link( $input = null ): bool {
	$user = wp_get_current_user();
	return $user instanceof WP_User && $user->exists() && wpagent_companion_is_safe_connection_user( $user );
}

function wpagent_companion_browser_link_option( int $user_id ): string {
	return WPAGENT_COMPANION_BROWSER_LINK_PREFIX . $user_id;
}

/**
 * Signs the persisted authorization context without retaining the bearer token.
 * A database write alone therefore cannot retarget an intercepted credential.
 */
function wpagent_companion_browser_link_binding_hash( array $record ): string {
	$binding = array(
		'action'      => (string) ( $record['action'] ?? '' ),
		'blog_id'     => (int) ( $record['blog_id'] ?? 0 ),
		'created_at'  => (int) ( $record['created_at'] ?? 0 ),
		'destination' => (string) ( $record['destination'] ?? '' ),
		'expires'     => (int) ( $record['expires'] ?? 0 ),
		'token_hash'  => (string) ( $record['token_hash'] ?? '' ),
		'user_id'     => (int) ( $record['user_id'] ?? 0 ),
	);
	return hash_hmac( 'sha256', (string) wp_json_encode( $binding, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), wp_salt( 'auth' ) );
}

/**
 * Builds the handoff URL from the persisted site URL without applying the
 * filterable login_url or site_url hooks.
 *
 * @return string|WP_Error
 */
function wpagent_companion_browser_login_url() {
	global $wpdb;
	if ( defined( 'WP_SITEURL' ) ) {
		$site_url = WP_SITEURL;
	} else {
		// Security boundary: bypass option/site URL filters that could redirect the credential handoff.
		$site_url = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'siteurl' ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- trusted core table name.
		);
	}
	if ( ! is_string( $site_url ) || '' === $site_url || trim( $site_url ) !== $site_url || preg_match( '/[\x00-\x20\x7f]/', $site_url ) || preg_match( '/%0[ad]/i', $site_url ) ) {
		return new WP_Error( 'wpagent_browser_link_url_invalid', __( 'Die sichere WordPress-Anmeldeadresse konnte nicht ermittelt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$site_url = set_url_scheme( $site_url, 'login' );
	$base     = wp_parse_url( $site_url );
	if ( ! is_array( $base ) || ! in_array( strtolower( (string) ( $base['scheme'] ?? '' ) ), array( 'http', 'https' ), true ) || '' === (string) ( $base['host'] ?? '' ) || isset( $base['user'] ) || isset( $base['pass'] ) || isset( $base['query'] ) || isset( $base['fragment'] ) ) {
		return new WP_Error( 'wpagent_browser_link_url_invalid', __( 'Die sichere WordPress-Anmeldeadresse konnte nicht ermittelt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	return untrailingslashit( $site_url ) . '/wp-login.php';
}

/** @return string|WP_Error */
function wpagent_companion_browser_destination_url( string $destination ) {
	$destinations = wpagent_companion_browser_destinations();
	if ( ! isset( $destinations[ $destination ] ) ) {
		return new WP_Error( 'wpagent_browser_link_url_invalid', __( 'Die sichere WordPress-Backend-Adresse konnte nicht ermittelt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$login_url = wpagent_companion_browser_login_url();
	if ( is_wp_error( $login_url ) || ! str_ends_with( $login_url, '/wp-login.php' ) ) {
		return new WP_Error( 'wpagent_browser_link_url_invalid', __( 'Die sichere WordPress-Backend-Adresse konnte nicht ermittelt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$base = substr( $login_url, 0, -strlen( '/wp-login.php' ) );
	return $base . '/wp-admin/' . $destinations[ $destination ];
}

/**
 * Löscht genau den gelesenen Datensatz. Dadurch kann ein alter Link keinen
 * inzwischen neu erstellten Link desselben Nutzers verbrauchen.
 */
function wpagent_companion_delete_browser_link_record( string $option, array $record ): bool {
	global $wpdb;
	$deleted = $wpdb->delete(
		$wpdb->options,
		array( 'option_name' => $option, 'option_value' => maybe_serialize( $record ) ),
		array( '%s', '%s' )
	);
	if ( 1 === $deleted ) {
		wp_cache_delete( $option, 'options' );
		return true;
	}
	return false;
}

/** @return array|WP_Error */
function wpagent_exec_create_browser_link( $input = array() ) {
	if ( ! wpagent_perm_browser_link() ) {
		return new WP_Error( 'wpagent_browser_link_forbidden', __( 'Backend-Einmal-Links sind nur für authentifizierte Nutzer mit Inhaltsrechten ohne Administrator- oder Super-Administratorrechte verfügbar.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! is_ssl() && ! in_array( wp_get_environment_type(), array( 'local', 'development' ), true ) ) {
		return new WP_Error( 'wpagent_browser_link_https_required', __( 'Backend-Einmal-Links sind außerhalb lokaler Entwicklungsumgebungen nur über HTTPS verfügbar.', 'wp-agent-companion' ), array( 'status' => 503 ) );
	}
	$input = is_array( $input ) ? $input : array();
	if ( array_key_exists( 'destination', $input ) && ( ! is_string( $input['destination'] ) || strlen( $input['destination'] ) > 16 ) ) {
		return new WP_Error( 'wpagent_browser_link_destination', __( 'Unbekanntes Backend-Ziel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$destination  = sanitize_key( $input['destination'] ?? 'dashboard' );
	$destinations = wpagent_companion_browser_destinations();
	if ( ! isset( $destinations[ $destination ] ) ) {
		return new WP_Error( 'wpagent_browser_link_destination', __( 'Unbekanntes Backend-Ziel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$login_url = wpagent_companion_browser_login_url();
	if ( is_wp_error( $login_url ) ) {
		return $login_url;
	}

	$user_id = get_current_user_id();
	try {
		$token = rtrim( strtr( base64_encode( random_bytes( 32 ) ), '+/', '-_' ), '=' );
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_browser_link_entropy', __( 'Für den Backend-Einmal-Link konnte kein sicherer Zufallswert erzeugt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$created_at = time();
	$expires    = $created_at + WPAGENT_COMPANION_BROWSER_LINK_TTL;
	$record  = array(
		'action'      => 'browser_login',
		'blog_id'     => get_current_blog_id(),
		'token_hash'  => hash( 'sha256', $token ),
		'expires'     => $expires,
		'destination' => $destination,
		'created_at'  => $created_at,
		'user_id'     => $user_id,
	);
	$record['binding_hash'] = wpagent_companion_browser_link_binding_hash( $record );
	update_option( wpagent_companion_browser_link_option( $user_id ), $record, false );
	$option = wpagent_companion_browser_link_option( $user_id );
	$stored = get_option( $option, array() );
	if ( ! is_array( $stored ) || $stored !== $record ) {
		if ( is_array( $stored ) && hash_equals( $record['token_hash'], (string) ( $stored['token_hash'] ?? '' ) ) ) {
			wpagent_companion_delete_browser_link_record( $option, $stored );
		}
		return new WP_Error( 'wpagent_browser_link_persist_failed', __( 'Der Backend-Einmal-Link konnte nicht verifiziert gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$url = add_query_arg(
		array( 'wpagently_browser' => '1', 'user' => $user_id ),
		$login_url
	);
	return array(
		'url' => $url, 'expires_at' => gmdate( 'c', $expires ), 'user_id' => $user_id,
		'destination' => $destination, 'one_time' => true,
		'headers' => array( WPAGENT_COMPANION_BROWSER_LINK_HEADER => $token ),
	);
}

/** @return array|WP_Error */
function wpagent_exec_revoke_browser_link( $input = array() ) {
	if ( ! wpagent_perm_browser_link() ) {
		return new WP_Error( 'wpagent_browser_link_forbidden', __( 'Backend-Einmal-Links sind nur für authentifizierte Nutzer mit Inhaltsrechten ohne Administrator- oder Super-Administratorrechte verfügbar.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$option = wpagent_companion_browser_link_option( get_current_user_id() );
	$record = get_option( $option, false );
	$existed = is_array( $record );
	if ( $existed && ! wpagent_companion_delete_browser_link_record( $option, $record ) ) {
		return new WP_Error( 'wpagent_browser_link_revoke_failed', __( 'Der Backend-Einmal-Link konnte nicht widerrufen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return array( 'revoked' => $existed );
}

function wpagent_companion_browser_link_record_is_valid( array $record, WP_User $user ): bool {
	$expected_keys = array( 'action', 'binding_hash', 'blog_id', 'created_at', 'destination', 'expires', 'token_hash', 'user_id' );
	$actual_keys   = array_keys( $record );
	sort( $actual_keys, SORT_STRING );
	if ( $actual_keys !== $expected_keys ) {
		return false;
	}
	if ( ! is_int( $record['user_id'] ) || (int) $record['user_id'] !== (int) $user->ID || ! is_int( $record['blog_id'] ) || (int) $record['blog_id'] !== get_current_blog_id() || ! is_string( $record['action'] ) || ! hash_equals( 'browser_login', $record['action'] ) ) {
		return false;
	}
	if ( ! is_string( $record['token_hash'] ) || ! preg_match( '/^[a-f0-9]{64}$/D', $record['token_hash'] ) || ! is_string( $record['binding_hash'] ) || ! preg_match( '/^[a-f0-9]{64}$/D', $record['binding_hash'] ) || ! is_int( $record['created_at'] ) || $record['created_at'] < 1 || $record['created_at'] > time() || ! is_int( $record['expires'] ) || $record['expires'] !== $record['created_at'] + WPAGENT_COMPANION_BROWSER_LINK_TTL ) {
		return false;
	}
	if ( ! hash_equals( wpagent_companion_browser_link_binding_hash( $record ), $record['binding_hash'] ) ) {
		return false;
	}
	$destination = is_string( $record['destination'] ) ? $record['destination'] : '';
	return isset( wpagent_companion_browser_destinations()[ $destination ] );
}

/**
 * Prüft und verbraucht einen Link. Die erfolgreiche delete_option()-Operation
 * ist Teil der Prüfung, damit zwei gleichzeitige Aufrufe nicht beide einloggen.
 *
 * @return array{user_id:int,destination:string}|WP_Error
 */
function wpagent_companion_consume_browser_link( int $user_id, string $token ) {
	$user   = get_user_by( 'id', $user_id );
	$option = wpagent_companion_browser_link_option( $user_id );
	$record = get_option( $option, false );
	if ( ! is_array( $record ) ) {
		return new WP_Error( 'wpagent_browser_link_invalid', __( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! wpagent_companion_gate_open() || ! wpagent_companion_ability_enabled( 'wp-agent/create-browser-link' ) || ! $user instanceof WP_User || ! wpagent_companion_is_safe_connection_user( $user ) || ! wpagent_companion_browser_link_record_is_valid( $record, $user ) ) {
		wpagent_companion_delete_browser_link_record( $option, $record );
		return new WP_Error( 'wpagent_browser_link_invalid', __( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( (int) ( $record['expires'] ?? 0 ) <= time() ) {
		wpagent_companion_delete_browser_link_record( $option, $record );
		return new WP_Error( 'wpagent_browser_link_invalid', __( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$expected = (string) ( $record['token_hash'] ?? '' );
	if ( '' === $token || strlen( $token ) > 128 || 64 !== strlen( $expected ) || ! hash_equals( $expected, hash( 'sha256', $token ) ) ) {
		return new WP_Error( 'wpagent_browser_link_invalid', __( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$destinations = wpagent_companion_browser_destinations();
	$destination  = sanitize_key( (string) ( $record['destination'] ?? '' ) );
	if ( ! isset( $destinations[ $destination ] ) || ! wpagent_companion_delete_browser_link_record( $option, $record ) ) {
		return new WP_Error( 'wpagent_browser_link_invalid', __( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	return array( 'user_id' => $user_id, 'destination' => $destination );
}

function wpagent_companion_browser_link_request_token(): string {
	$raw = $_SERVER['HTTP_X_WPAGENT_BROWSER_TOKEN'] ?? '';
	if ( ! is_string( $raw ) ) {
		return '';
	}
	$token = trim( wp_unslash( $raw ) );
	return preg_match( '/^[A-Za-z0-9_-]{43}$/D', $token ) ? $token : '';
}

function wpagent_companion_handle_browser_link(): void {
	if ( ! isset( $_GET['wpagently_browser'] ) || ! is_string( $_GET['wpagently_browser'] ) || '1' !== $_GET['wpagently_browser'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the one-time token is the authorization credential.
		return;
	}
	$user_raw = $_GET['user'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	$user_id = is_scalar( $user_raw ) ? absint( $user_raw ) : 0;
	$token   = wpagent_companion_browser_link_request_token();
	$result  = wpagent_companion_consume_browser_link( $user_id, $token );
	if ( is_wp_error( $result ) ) {
		wp_die( esc_html__( 'Der Backend-Einmal-Link ist ungültig oder abgelaufen.', 'wp-agent-companion' ), esc_html__( 'Anmeldung fehlgeschlagen', 'wp-agent-companion' ), array( 'response' => 403 ) );
	}
	$destination_url = wpagent_companion_browser_destination_url( $result['destination'] );
	if ( is_wp_error( $destination_url ) ) {
		wp_die( esc_html__( 'Die sichere Backend-Adresse konnte nicht ermittelt werden.', 'wp-agent-companion' ), esc_html__( 'Anmeldung fehlgeschlagen', 'wp-agent-companion' ), array( 'response' => 500 ) );
	}
	wp_set_current_user( $result['user_id'] );
	wp_set_auth_cookie( $result['user_id'], false, is_ssl() );
	nocache_headers();
	header( 'Referrer-Policy: no-referrer' );
	// $destination_url is derived exclusively from the persisted, validated
	// site URL and a fixed destination map. Avoid WordPress redirect helpers:
	// their filterable fallbacks can reintroduce a target outside this token's
	// signed origin boundary.
	header( 'Location: ' . $destination_url, true, 302 );
	exit;
}
add_action( 'login_init', 'wpagent_companion_handle_browser_link' );
