<?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', 'enum' => array_keys( wpagent_companion_browser_destinations() ), 'default' => 'dashboard' ),
			),
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'url' => array( 'type' => 'string' ), 'expires_at' => array( 'type' => 'string' ),
				'user_id' => array( 'type' => 'integer' ), 'destination' => array( 'type' => 'string' ),
				'one_time' => array( 'type' => 'boolean' ),
				'headers' => array(
					'type'                 => 'object',
					'properties'           => array( WPAGENT_COMPANION_BROWSER_LINK_HEADER => array( 'type' => 'string' ) ),
					'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(), 'default' => array() ),
		'output_schema'       => array( 'type' => 'object', 'properties' => array( 'revoked' => array( 'type' => 'boolean' ) ) ),
		'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' => true ) ),
	);
}

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;
}

/**
 * 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 ) ) {
		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';
}

/**
 * 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'] ) ) {
		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 ) );
	}
	$expires = time() + WPAGENT_COMPANION_BROWSER_LINK_TTL;
	$record  = array(
		'token_hash'  => hash( 'sha256', $token ),
		'expires'     => $expires,
		'destination' => $destination,
		'created_at'  => time(),
	);
	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 );
}

/**
 * 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_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 ) );
	}
	wp_set_current_user( $result['user_id'] );
	wp_set_auth_cookie( $result['user_id'], false, is_ssl() );
	nocache_headers();
	header( 'Referrer-Policy: no-referrer' );
	wp_safe_redirect( admin_url( wpagent_companion_browser_destinations()[ $result['destination'] ] ) );
	exit;
}
add_action( 'login_init', 'wpagent_companion_handle_browser_link' );
