<?php
/** HTTP-Handler für direkte, größenbegrenzte Medien-Uploads. */

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

/** @return never */
function wpagent_companion_direct_media_upload_error( WP_Error $error ): void {
	$data   = $error->get_error_data();
	$status = is_array( $data ) ? (int) ( $data['status'] ?? 500 ) : 500;
	if ( $status < 400 || $status > 599 ) {
		$status = 500;
	}
	nocache_headers();
	header( 'Referrer-Policy: no-referrer' );
	header( 'X-Content-Type-Options: nosniff' );
	$response = array(
		'code'    => $error->get_error_code(),
		'message' => $error->get_error_message(),
	);
	if ( 'wpagent_direct_upload_recovery_required' === $error->get_error_code() && is_array( $data ) ) {
		$attachment_id = isset( $data['attachment_id'] ) && is_int( $data['attachment_id'] ) ? $data['attachment_id'] : 0;
		if ( $attachment_id > 0 ) {
			$response['attachment_id'] = $attachment_id;
		}
		$response['recovery_required'] = true;
	}
	wp_send_json_error( $response, $status );
}

/** @return string|WP_Error */
function wpagent_companion_direct_media_upload_actual_mime( string $path ) {
	if ( ! function_exists( 'finfo_open' ) || ! defined( 'FILEINFO_MIME_TYPE' ) ) {
		return new WP_Error( 'wpagent_direct_upload_mime', __( 'Der tatsächliche Dateityp konnte auf diesem Server nicht sicher geprüft werden.', 'wp-agent-companion' ), array( 'status' => 415 ) );
	}
	try {
		$finfo = finfo_open( FILEINFO_MIME_TYPE );
		$mime  = is_resource( $finfo ) || $finfo instanceof finfo ? finfo_file( $finfo, $path ) : false;
	} catch ( Throwable $error ) {
		$mime = false;
	}
	$mime = is_string( $mime ) ? strtolower( trim( $mime ) ) : '';
	if ( '' === $mime || strlen( $mime ) > 255 || ! preg_match( '~^[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*$~D', $mime ) ) {
		return new WP_Error( 'wpagent_direct_upload_mime', __( 'Der tatsächliche Dateityp konnte nicht sicher geprüft werden.', 'wp-agent-companion' ), array( 'status' => 415 ) );
	}
	return $mime;
}

/**
 * Verifies the persisted attachment against the bounded, one-time upload record.
 *
 * A failed verification deliberately preserves the attachment. WordPress has no atomic
 * compare-and-delete primitive, so deleting after any read-back could erase a concurrent
 * native editor or provider change.
 *
 * @return array{attached_file:string}|false
 */
function wpagent_companion_direct_media_upload_owned_attachment_matches( int $attachment_id, array $record, int $expected_bytes, string $expected_file_hash ) {
	if ( $attachment_id < 1 || $expected_bytes < 1 || ! preg_match( '/^[a-f0-9]{64}$/D', $expected_file_hash ) || ! isset( $record['user_id'], $record['post_id'], $record['expected_mime'], $record['alt'] ) || ! is_int( $record['user_id'] ) || ! is_int( $record['post_id'] ) || ! is_string( $record['expected_mime'] ) || ! is_string( $record['alt'] ) ) {
		return false;
	}
	global $wpdb;
	$post = $wpdb->get_row(
		$wpdb->prepare( "SELECT post_type, post_mime_type, post_parent, post_author FROM {$wpdb->posts} WHERE ID = %d", $attachment_id ),
		ARRAY_A
	);
	if ( ! is_array( $post ) || 'attachment' !== (string) ( $post['post_type'] ?? '' ) ) {
		return false;
	}
	$stored_alt = '';
	if ( str_starts_with( (string) $record['expected_mime'], 'image/' ) ) {
		$stored_alt = $wpdb->get_var(
			$wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s ORDER BY meta_id ASC LIMIT 1", $attachment_id, '_wp_attachment_image_alt' )
		);
		if ( ! is_string( $stored_alt ) ) {
			return false;
		}
	}
	wp_cache_delete( $attachment_id, 'post_meta' );
	$attached_file = get_attached_file( $attachment_id );
	if ( ! is_string( $attached_file ) || '' === $attached_file || ! is_file( $attached_file ) ) {
		return false;
	}
	$file_size = filesize( $attached_file );
	if ( false === $file_size || $expected_bytes !== (int) $file_size ) {
		return false;
	}
	$file_hash = hash_file( 'sha256', $attached_file );
	if ( ! is_string( $file_hash ) || ! preg_match( '/^[a-f0-9]{64}$/D', $file_hash ) || ! hash_equals( $expected_file_hash, $file_hash ) ) {
		return false;
	}
	$mime_type = (string) ( $post['post_mime_type'] ?? '' );
	if ( ! hash_equals( $record['expected_mime'], $mime_type ) || (int) $record['post_id'] !== (int) ( $post['post_parent'] ?? -1 ) || (int) $record['user_id'] !== (int) ( $post['post_author'] ?? -1 ) || ( str_starts_with( $mime_type, 'image/' ) && ! hash_equals( $record['alt'], $stored_alt ) ) ) {
		return false;
	}
	return array( 'attached_file' => $attached_file );
}

/** @return WP_Error */
function wpagent_companion_direct_media_upload_recovery_required( int $attachment_id = 0 ): WP_Error {
	$data = array( 'status' => 409, 'recovery_required' => true );
	if ( $attachment_id > 0 ) {
		$data['attachment_id'] = $attachment_id;
	}
	return new WP_Error( 'wpagent_direct_upload_recovery_required', __( 'Der direkte Upload konnte nach dem Speichern nicht vollständig verifiziert werden. Der angelegte Anhang bleibt unverändert. Bitte prüfe ihn manuell.', 'wp-agent-companion' ), $data );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_companion_stream_direct_media_upload( array $record ) {
	require_once ABSPATH . 'wp-admin/includes/file.php';
	require_once ABSPATH . 'wp-admin/includes/image.php';
	require_once ABSPATH . 'wp-admin/includes/media.php';

	$source = fopen( 'php://input', 'rb' );
	if ( false === $source ) {
		return new WP_Error( 'wpagent_direct_upload_stream', __( 'Der Upload-Datenstrom konnte nicht geöffnet werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$temp = wp_tempnam( (string) $record['filename'] );
	if ( ! is_string( $temp ) || '' === $temp ) {
		fclose( $source );
		return new WP_Error( 'wpagent_direct_upload_temp', __( 'Für den direkten Upload konnte keine temporäre Datei angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$target = fopen( $temp, 'wb' );
	if ( false === $target ) {
		fclose( $source );
		wp_delete_file( $temp );
		return new WP_Error( 'wpagent_direct_upload_temp', __( 'Für den direkten Upload konnte keine temporäre Datei geöffnet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$bytes       = 0;
	$result      = null;
	$stream_hash = hash_init( 'sha256' );
	try {
		while ( ! feof( $source ) ) {
			$chunk = fread( $source, 65536 );
			if ( false === $chunk ) {
				$result = new WP_Error( 'wpagent_direct_upload_read', __( 'Der direkte Upload konnte nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
				break;
			}
			if ( '' === $chunk ) {
				if ( feof( $source ) ) {
					break;
				}
				$result = new WP_Error( 'wpagent_direct_upload_read', __( 'Der direkte Upload konnte nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
				break;
			}
			$chunk_bytes = strlen( $chunk );
			if ( $bytes + $chunk_bytes > (int) $record['max_bytes'] ) {
				$result = new WP_Error( 'wpagent_direct_upload_too_large', __( 'Die Datei überschreitet die freigegebene Upload-Größe.', 'wp-agent-companion' ), array( 'status' => 413 ) );
				break;
			}
			$offset = 0;
			while ( $offset < $chunk_bytes ) {
				$written = fwrite( $target, substr( $chunk, $offset ) );
				if ( false === $written || 0 === $written ) {
					$result = new WP_Error( 'wpagent_direct_upload_write', __( 'Der direkte Upload konnte nicht vollständig gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
					break 2;
				}
				$offset += $written;
			}
			$bytes += $chunk_bytes;
			hash_update( $stream_hash, $chunk );
		}
	} catch ( Throwable $error ) {
		$result = new WP_Error( 'wpagent_direct_upload_stream', __( 'Der direkte Upload ist beim Lesen des Datenstroms fehlgeschlagen.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	} finally {
		fclose( $source );
		fclose( $target );
	}
	if ( $result instanceof WP_Error ) {
		wp_delete_file( $temp );
		return $result;
	}
	if ( $bytes < 1 ) {
		wp_delete_file( $temp );
		return new WP_Error( 'wpagent_direct_upload_empty', __( 'Der direkte Upload enthält keine Datei.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$content_hash = hash_final( $stream_hash );
	if ( ! is_string( $content_hash ) || ! preg_match( '/^[a-f0-9]{64}$/D', $content_hash ) ) {
		wp_delete_file( $temp );
		return new WP_Error( 'wpagent_direct_upload_stream', __( 'Der direkte Upload konnte nicht sicher verarbeitet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$attachment_id = 0;
	try {
		$allowed = get_allowed_mime_types( (int) $record['user_id'] );
		$checked = wp_check_filetype_and_ext( $temp, (string) $record['filename'], $allowed );
		$actual_mime = wpagent_companion_direct_media_upload_actual_mime( $temp );
		if ( is_wp_error( $actual_mime ) || empty( $checked['ext'] ) || empty( $checked['type'] ) || ! hash_equals( strtolower( (string) $record['expected_mime'] ), strtolower( (string) $checked['type'] ) ) || ! hash_equals( strtolower( (string) $record['expected_mime'] ), $actual_mime ) ) {
			return new WP_Error( 'wpagent_direct_upload_mime', __( 'Der Dateiinhalt stimmt nicht mit dem freigegebenen Dateityp überein.', 'wp-agent-companion' ), array( 'status' => 415 ) );
		}

		$attachment_id = media_handle_sideload(
			array( 'name' => (string) $record['filename'], 'tmp_name' => $temp, 'error' => 0, 'size' => $bytes ),
			(int) $record['post_id'],
			null,
			array( 'post_author' => (int) $record['user_id'] )
		);
		if ( is_wp_error( $attachment_id ) ) {
			return new WP_Error( 'wpagent_direct_upload_media', __( 'Der direkte Upload ist bei der Medienverarbeitung fehlgeschlagen.', 'wp-agent-companion' ), array( 'status' => 415 ) );
		}
		$attachment_id = (int) $attachment_id;

		if ( str_starts_with( (string) $record['expected_mime'], 'image/' ) ) {
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', (string) $record['alt'] );
		}
		// A matching size alone is not a verified upload. Re-hash the stored
		// attachment before reporting success so a same-size file replacement
		// cannot inherit the streamed content's hash.
		$owned_attachment = wpagent_companion_direct_media_upload_owned_attachment_matches( $attachment_id, $record, $bytes, $content_hash );
		if ( ! is_array( $owned_attachment ) || ! isset( $owned_attachment['attached_file'] ) || ! is_string( $owned_attachment['attached_file'] ) ) {
			return wpagent_companion_direct_media_upload_recovery_required( $attachment_id );
		}
		$attached_file = $owned_attachment['attached_file'];
		$url           = (string) wp_get_attachment_url( $attachment_id );
		if ( '' === $url ) {
			return wpagent_companion_direct_media_upload_recovery_required( $attachment_id );
		}

		return array(
			'attachment_id' => $attachment_id,
			'post_id'       => (int) $record['post_id'],
			'filename'      => basename( $attached_file ),
			'mime_type'     => (string) $record['expected_mime'],
			'bytes'         => $bytes,
			'alt'           => str_starts_with( (string) $record['expected_mime'], 'image/' ) ? (string) $record['alt'] : '',
			'url'           => $url,
			'verified'      => true,
		);
	} catch ( Throwable $error ) {
		if ( $attachment_id > 0 ) {
			return wpagent_companion_direct_media_upload_recovery_required( $attachment_id );
		}
		return new WP_Error( 'wpagent_direct_upload_processing', __( 'Der direkte Upload ist bei der Medienverarbeitung fehlgeschlagen.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	} finally {
		if ( is_file( $temp ) ) {
			wp_delete_file( $temp );
		}
	}
}

function wpagent_companion_handle_direct_media_upload(): void {
	if ( 'POST' !== strtoupper( (string) ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) ) {
		header( 'Allow: POST' );
		wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_method', __( 'Der direkte Upload akzeptiert ausschließlich POST-Anfragen.', 'wp-agent-companion' ), array( 'status' => 405 ) ) );
	}
	$content_type = strtolower( trim( explode( ';', (string) ( $_SERVER['CONTENT_TYPE'] ?? '' ), 2 )[0] ) );
	$encoding     = strtolower( trim( (string) ( $_SERVER['HTTP_CONTENT_ENCODING'] ?? $_SERVER['CONTENT_ENCODING'] ?? '' ) ) );
	if ( 'application/octet-stream' !== $content_type || ( '' !== $encoding && 'identity' !== $encoding ) ) {
		wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_encoding', __( 'Der direkte Upload erwartet einen unveränderten application/octet-stream-Datenstrom.', 'wp-agent-companion' ), array( 'status' => 415 ) ) );
	}
	$user_raw = $_GET['user'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- der Einmal-Header ist der Autorisierungsnachweis.
	$user_id  = is_scalar( $user_raw ) ? absint( $user_raw ) : 0;
	$token    = wpagent_companion_direct_media_upload_request_token();
	$record   = wpagent_companion_peek_direct_media_upload( $user_id, $token );
	if ( is_wp_error( $record ) ) {
		wpagent_companion_direct_media_upload_error( $record );
	}
	$length_raw = $_SERVER['CONTENT_LENGTH'] ?? null;
	if ( null !== $length_raw ) {
		if ( ! is_scalar( $length_raw ) || ! ctype_digit( (string) $length_raw ) ) {
			wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_length', __( 'Die angegebene Upload-Größe ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) ) );
		}
		$length = (int) $length_raw;
		if ( $length < 1 ) {
			wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_empty', __( 'Der direkte Upload enthält keine Datei.', 'wp-agent-companion' ), array( 'status' => 400 ) ) );
		}
		if ( $length > (int) $record['max_bytes'] ) {
			wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_too_large', __( 'Die Datei überschreitet die freigegebene Upload-Größe.', 'wp-agent-companion' ), array( 'status' => 413 ) ) );
		}
	}
	$record = wpagent_companion_consume_direct_media_upload( $user_id, $token );
	if ( is_wp_error( $record ) ) {
		wpagent_companion_direct_media_upload_error( $record );
	}

	$previous_user = get_current_user_id();
	wp_set_current_user( (int) $record['user_id'] );
	if ( (int) $record['post_id'] > 0 && ( ! get_post( (int) $record['post_id'] ) || ! current_user_can( 'edit_post', (int) $record['post_id'] ) ) ) {
		wp_set_current_user( $previous_user );
		wpagent_companion_direct_media_upload_error( new WP_Error( 'wpagent_direct_upload_parent_forbidden', __( 'Der Eltern-Beitrag ist nicht mehr verfügbar oder bearbeitbar.', 'wp-agent-companion' ), array( 'status' => 403 ) ) );
	}
	try {
		$result = wpagent_companion_stream_direct_media_upload( $record );
	} catch ( Throwable $error ) {
		$result = new WP_Error( 'wpagent_direct_upload_processing', __( 'Der direkte Upload ist bei der Medienverarbeitung fehlgeschlagen.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	} finally {
		wp_set_current_user( $previous_user );
	}
	if ( is_wp_error( $result ) ) {
		wpagent_companion_direct_media_upload_error( $result );
	}
	nocache_headers();
	header( 'Referrer-Policy: no-referrer' );
	header( 'X-Content-Type-Options: nosniff' );
	wp_send_json_success( $result, 201 );
}
add_action( 'admin_post_nopriv_wpagently_direct_media_upload', 'wpagent_companion_handle_direct_media_upload' );
add_action( 'admin_post_wpagently_direct_media_upload', 'wpagent_companion_handle_direct_media_upload' );
